STL源码解析阅读理解:Allocator

Allocator(空间配置器)

  • 学习大纲
  • SGI 空间配置器的种类及区别
    • ::operator new和::operator delete的操作步骤
  • SGI标准的空间配置器,std::allocator
    • allocate()
    • deallocate()
  • SGI特殊的空间配置器,std::alloc
    • 对象的构造和析构(待补充)
      • construct()
      • destroy()
    • 空间的配置与释放
      • 一级空间配置器__malloc_alloc_template
        • allocate()
        • deallocate()
        • reallocate()
        • 仿真c++的set_new_handler()
        • oom_alloc
        • oom_realloc
      • 二级空间配置器__default_alloc_template
        • 二级空间配置器配置空间的流程图:
        • free_list的结构
        • 核心代码介绍allocate、deallocate、refill、chunk_alloc
        • 内存池
        • chunk_alloc处理流程图

前言
这本书去年就有看过一点,但是那个时候读源码读的太痛苦,遂放弃了一段时间,后来断断续续又看了点,但是每次碰到allocator部分的知识依旧是云里雾里。现在决定从头开始再看一遍。
以下内容是我阅读 侯捷《STL源码剖析》 这本书的笔记。如果有理解有误的地方还望各位大佬能够不吝指出。

在我们正式开始介绍Allcoator(空间配置器)之前先想一个问题。我们为什么要有空间配置器?这个问题我知道很简单,顾名思义,当然是为了分配空间啦。那分配空间的目的能?那当然是为了存储数据啦。就像书中所言” 整个STL的操作对象(所有数值)都存放在容器之内,而容器一定需要配置空间以存放资料

这里将allocator定义为空间配置器而不是内存配置器的原因:因为空间不一定是内存,空间也可以是磁盘或其它辅助存储介质。【书中原话】

学习大纲

STL源码解析阅读理解:Allocator_第1张图片

SGI 空间配置器的种类及区别

SGI的空间配置器有两种:

  1. SGI标准的空间配置器,std::allocator
  2. SGI特殊的空间配置器,std::alloc

它们两者之间的区别:

  1. 它们的底层实现不同。std::allocator中的底层是用operator new和operator delete实现的空间配置和释放;而std::alloc中使用的则是malloc和free完成的配置和释放
  2. std::allocator中,内存配置和对象构造封装在allocate函数中一步完成的(一会介绍原因),内存释放和对象析构也是封装在deallocate函数中一步完成的。而std::alloc中,内存的配置与释放和对象的构造与析构都是使用单独的函数实现的。

::operator new和::operator delete的操作步骤

我们平时调用的new/delete实际上就是::operator new和::operator delete这两个函数。
我们来看下面的语句:

A *a = new A;	// 分配内存,然后构造对象
delete a;		// 将对象析构,然后释放内存

这个语句等号右边的部分(new A)中实际上进行了两个操作:

  1. 调用::operator new分配内存;
  2. 调用A::A()构造对象。

同理,对于delete a;而言也包含两个操作

  1. 调用A::~A()将对象析构;
  2. 调用::operator delete释放内存。

这也就为什么我们说在std::allocator中,内存分配和对象构造是被封装在同一个函数中的原因了。因为它的allocate函数的底层实现就是通过operator new来完成的,人家operator new本身就包含了内存分配和对象构造啦,你总不能把人家强行扯开吧~

下面分别介绍std::allocator和std::alloc。

SGI标准的空间配置器,std::allocator

allocate()

template<class T>
inline T* allocate(ptrdiff_t, size. T*){
	set_new_handler(0);		// 处理内存不足,这里被设置为0,表示遇到内存不足情况的时候直接抛出bad_alloc异常
	T* tmp = (T*) (::operator new((size_t)(size * sizeof(T))));	//直接调用new分配内存,并且构造对象
	// 当内存分配失败时会打印out of memory,并且强制退出
	if (tmp == 0){
		cerr << "out of memory" << endl;
		exit(1);
	}
	return tmp;
}

deallocate()

template<class T>
inline void deallocate(T* buffer){
	::opertator delete(buffer);		// 直接调用delete析构对象,并释放内存
}

SGI特殊的空间配置器,std::alloc

该配置器中对象的构造与析构放在头文件:#incldue
该配置器中空间的配置与释放放在头文件:#incldue

该配置器的空间分配又分为两种:

  1. 一级空间配置器
  2. 二级空间配置器

对象的构造和析构(待补充)

construct()

需要用到头文件 #include中的placement new

#includde 

// 构造对象
template<class T1, class T2>
inline void construct(T1 *p, T2& value){
	new(p) T1(value);     // 调用T1::T1()构造函数,并将初始值设定到指针所指的空间上,这个由placement new完成
}

destroy()

destroy()实现析构对象。包括两个版本:

  1. 仅接受一个指针,也就是仅析构一个对象;
  2. 接受两个迭代器,析构这两个迭代器之间的值,一般是[first, last),左闭右开。
// 第一个版本的destroy()
template <class T>
inline void destroy(T* pointer){
	pointer->~T();
}


// 第二个版本的destory()
template < class ForwardIterator>
inline void destroy(ForwardIterator first, ForwardIterator last){
// 两个参数,[first, last}范围内的对象析构掉
	__destroy(first, last, value_type(first));	 // 最后一个参数主要是为了判断该迭代器的类型
}

// 判断元素的数值类型是否由trivial destructor
template < class ForwardIterator, class T?
inline void __destroy(ForwardIterator first, ForwardIterator last, T*){
	typedef typename __type_traits<T>::has_trivial_destructor trivial_dedstructor;	// 重新定义typename __type_traits::has_trivial_destructor,使用typename 修饰表示它是一个类型,而不是一个变量
	__destroy_aux(first, last, trivial_destroy());
}

// 如果元素的类型有non-trivial destructor
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __false_type){
	for ( ; first < last; ++first)
		destroy(&*first);			// 析构[first, last}范围内的所有对象
}

// 如果元素的类型有trivial destructor
template <class ForwardIterator>
inline void __destroy_aux(ForwardIterator first, ForwardIterator last, __true_type){}

【注】类型判断这里可能会有点不懂,但是没关系!以后会不上的!

空间的配置与释放

对象构造前的空间配置和对象析构后的空间释放,都是由负责的,SGI对此的设计理念如下:

  1. 想系统堆内存要求空间;
  2. 考虑多线程状态;
  3. 考虑内存不足的应变措施
  4. 考虑过多”小型区块“可能造成的内存碎片问题。

SGI设计双层配置器的原因: 考虑到小型区块可能造成的内存破碎问题,第一季配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略。

一级空间配置器__malloc_alloc_template

一级空间配置器的特点:

  1. allocate()直接使用malloc()分配内存,deallocte()直接使用fee()释放内存;
  2. 模拟c++的set_new_handler()来处理内存不足的情况。
    【注】关于set_new_handler的详细内容可用参考《Effective C++》中的条款49。这里你只需要直到它是处理内存不足情况即可。

接下来看看一级空间配置器是如何分配内存和释放内存,以及如何处理内存不足的情况的。

allocate()

代码实现如下:

static void* allocate(size_t n){
	void *result = malloc(n);   // 一级配置器直接使用malloc
	// 无法满足需求时,改为oom_malloc(n)
	if(result == 0) result = oom_malloc(n);
	return result;
}

代码分析:
line2:一级空间配置器中底层直接调用malloc进行空间配置,当空间分配失败的时候会返回空指针nullptr,当空间分配成功的时候会返回指向那片内存的指针;
line4:当空间分配失败的时候,调用处理内存不足情况的函数oom_malloc(),这个后面会介绍。

这部分的内容很少,也很简单,分配成功就返回内存地址,分配失败就调用处理失败的函数。

deallocate()

代码如下:

static void deallocate(void *p, size_t){
	free(p);    // 一级配置器直接使用free
}

这部分代码没啥好说的,就是直接使用free将分配的空间进行释放就行。

reallocate()

代码如下:

static void* reallocate(void *p, size_t, size_t new_sz){
	void * result = realloc(p, new_sz);   // 一级配置器直接使用realloc()
	// 无法满足要求时,改为oom_realloc(p, new_sz)
	if(result == 0) result = oom_realloc(p, new_sz);
	return result;
}

代码分析: 这部分和allocate也没啥太大区别
line2:直接调用realloc()函数重新分配内存即可,分配成功直接返回,分配失败转而调用处理失败的函数oom_realloc();

仿真c++的set_new_handler()

代码如下:

static void (*set_malloc_handler(void(*f)()))(){
// 函数的返回值是一个函数的地址
	void (* old) () = __malloc_alloc_oom_handler;
	__malloc_alloc_oom_handler = f;
	return (old);
}

代码分析:
这个代码是用来处理内存不足的情况的,在oom_malloc和oom_realloc中被循环调用。

oom_alloc

代码如下:

template <int inst>
void* __malloc_alloc_template<inst>::oom_malloc(size_t n){
    void (* my_malloc_handler)();	// 定义一个函数指针
    void* result;

    // 不断尝试释放、配置
    for (;;) {
        my_malloc_handler = malloc_alloc_oom_handler;
        // 如果“内存不足处理例程”没有被用户设定,那么my_malloc_handler就会为0,此时会丢出bad_alloc异常信息
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*my_malloc_handler)();  // 调用处理例程,企图释放内存
        result = malloc(n);   // 再次尝试配置内存
        if (result) return(result);
    }
}

代码分析: 当内存不足的时候,会循环调用line7-13的语句,它企图找到一个能够满足用户要求的内存大小。如果“内存不足处理例程”没有被用户设定,那么my_malloc_handler就会为0,此时会丢出bad_alloc异常信息,或者利用exit(1)强行终止程序。

oom_realloc

代码如下:

template <int inst>
void* __malloc_alloc_template<inst>::S_oom_realloc(void* p, size_t n)
{
    void (* my_malloc_handler)();
    void* result;

    //  给一个已经分配了地址的指针重新分配空间,参数 p 为原有的空间地址,n 是重新申请的地址长度
    for (;;) {
	// 当 "内存不足处理例程" 并未被客户设定,便调用 __THROW_BAD_ALLOC,丢出 bad_alloc 异常信息
        my_malloc_handler = malloc_alloc_oom_handler;
        if (0 == my_malloc_handler) { __THROW_BAD_ALLOC; }
        (*my_malloc_handler)();   // 调用处理例程,企图释放内存
        result = realloc(p, n);  // 再次尝试配置内存,扩大内存大小
        if (result) return(result);
    }
}

含义同oom_malloc

二级空间配置器__default_alloc_template

二级空间配置器的做法:

  1. 当用户申请的区块大小 >128bytes 时,就交给一级配置器处理
  2. 否则,以内存池管理,这种方法又称为层次配置器
    【层次配置器】每次配置一大块内存,并维护对应之自由链表。下次若再有相同大小的内存需求,就直接从free-lists中取出。

二级空间配置器的特点:

  • 使用free-lists进行内存管理,负责内存的分配和回收
  • 会主动将任何小额区块的内存需求量上调到8的倍数(比如用户要求30bytes,就会自动调整到32bytes)
  • 维护16个free-lists,管理的大小从8到128bytes。

二级空间配置器配置空间的流程图:

STL源码解析阅读理解:Allocator_第2张图片

free_list的结构

union obj{   // free_list的节点构造
	union obj * free_list_link;
	char client_data[1];
};

static obj * volatile free_list[16];
// volatile是用来阻止一些优化操作的,这里我们不要管它。
#0 #1 #2 #3 #4 #5 #6 #7 #8 #9 #10 #11 #12 #13 #14 #15

STL源码解析阅读理解:Allocator_第3张图片
每个free_list[i]都指向一个free_list的节点。

核心代码介绍allocate、deallocate、refill、chunk_alloc

根据前面介绍过的二级空间配置器的做法我们可用设计一个相关的allocate空间分配函数。

allocate:()

步骤:

  1. 判断区块大小,>128bytes就调用一级配置器
  2. <=128bytes就检查对应的free list;
  3. 如果free list之内有可用的区块,就直接拿来用
  4. 如果没有就将区块大小调至8倍数边界,然后调用refill(),准备为free list填充空间。

代码块:

// n必须大于0
static void * allocate(size_t n){
	obj * volatile * my_free_list;
	obj * result;
	
	// 如果申请的内存大小 > 128bytes 就调用一级配置器
	if (n > (size_t) __MAX_BYTES){
		return (malloc_alloc::allocate(n));
	}
	// 否则 < 128,寻找16个free-lists中适当的一个
	// FREELIST_INDEX()返回free-list的索引
	my_free_list = free_list + FREELIST_INDEX(n);
	result = *my_free_list;
	if (result == 0){
		// 没找到可用的free-list,准备重新填充free-list
		void* r = refill(ROUND_UP(n));
		return r;
	}
	// 调整free-list
	*my_free_list = result->free_list_link;
	return (result);
} 

deallocate()

步骤:

  1. 判断区块大小;
  2. 释放区块>128bytes调用第一级配置器;
  3. <=128bytes找到对应的free list,将区块回收

代码块:

// p不可以是0,p指向的是要回收的区块
static void deallocate(void * p, size_t n){
	obj *q = (obj *) p;
	obj * volatile * my_free_list;
	
	// 释放的内存> 128bytes,调用一级配置器
	if (n > (size_t) __MAX_BYTES){
		malloc_alloc::deallocate(p, n);
		return;
	}
	
	// 否则,<128bytes, 寻找对应的free-list
	my_free_list = free_list + FREELIST_INDEX(n);
	// 调整free-list,回收区块
	q->free_list_link = *my_free_list;
	*my_free_list = q;
} 

refill()

描述:

  1. 默认申请的区块数量是20个,但万一内存池空间不足,获取的区块数可能小于20
  • 我们分配的区块数量由chunk_alloc(n, nobjs)函数来获取
    • nobjs == 1,那么直接将该区块分配给用户,因此free_list中不需要加入新区块;
    • nobjs > 1,那么就将第一个区块分配给用户,其他的区块挂到相应的free_list中上。

代码块:

// 作用:将从内存池中申请的内存挂载到free_list上。
// 返回一个大小为n的对象,并且有时候会为适当的free-list增加节点
// 假设n已经适当上调到8的倍数
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n){
	int nobjs = 20;     // 默认申请的新区块为20个,这个区块也就是free_list[i]指向的内存块个数
	// 调用chunk_alloc(),尝试取得nobjs个区块作为free-list的新节点
	// 注意参数nobjs是按引用传递,那么经过chunk_alloc()函数之后,nobjs的值可能发生改变
	char * chunk = chunk_alloc(n, nobjs);  
	obj * volatile * my_free_list;
	obj * result;
	obj * current_obj, *next_obj;
	
	// 如果获得一个区块,这个区块就直接分配给调用者用,free-list无新节点
	if (nobjs == 1) return chunk;
	// 否则准备调整free-list,找到我应该把区块放置的那个位置,纳入新节点。
	my_free_list = free_list + FREELIST_INDEX(n);
	
	// 以下在chunk空间内建立free_list
	result = (obj*) chunk;         // 这一块准备返回给用户
	// 以下引导free_list指向新配置的空间(取自内存池)
	*my_free_list = next_obj = (obj*) (chunk+n);
	// 以下将free_list中的各个节点串接起来
	for (int i = 1; ; i++){         // 从1开始,因为第0个将返回给用户
		current_obj = next_obj;
		next_obj = (obj*) ((char*) next_obj + n);
		if (i == nobjs - 1){
			current_obj->free_list_link = 0;
			break;
		} else{
			current_obj ->free_list_link = next_obj;
		}
	}
	return result;
} 

内存池

从内存池中取出空间给free_list使用,时chunk_alloc()的工作
代码如下:

// 作用:从内存池中申请所需要的内存空间挂载到free_list上
//       同时处理可能发生内存池内存不够的情况
// 假设size已经适当调整到8的倍数
// 注意参数nobjs是按引用传递,默认为20
template < bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs){
	char * result;
	size_t total_bytes = size * nobjs;              // 总共要申请的空间
	size_t bytes_left = end_free - start_free;      // 内存池剩余空间
	
	// 1. 如果剩余空间能够满足需求
	if (bytes_left >= total_bytes){
		result = start_free;                 // 分配的空间范围为[start_free,  start_free + total_bytes)
		start_free += total_bytes;    // 将start_free向后偏移total_bytes,指向新的start位置
		return result;
	}
	// 2. 如果剩余空间不能满足需求量,但是足够供应一个及以上的区块
	else if ( bytes_left >= size){
		nobjs = bytes_left / size;       // nobjs为内存池能够供应的区块数量
		total_bytes = size * nobjs;   // 此时所能满足的总空间要更新以下啦
		result = start_free;
		start_free += total_bytes;
		return result;
	}
	// 3. 如果剩余的空间连一个需求量都不能满足
	else{
		size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
		// 3.1 试着让内存池中的残余零头还有利用价值
		if (bytes_left > 0){
			// 内存池内还有一些零头,先分配给适当的free_list
			// 首先寻找适当的free_list
			obj * volatile * my_free_list = free_list + FREELIST_INDEX(bytes_left);
			// 调整free_list, 将内存池中的残余空间编入
			((obj*) start_free)->free_list_link = *my_free_list;
			*my_free_list = (obj *) start_free;
		}
		
		// 3.2 配置heap空间,用来补充内存池
		start_free = (char *) malloc(bytes_to_get);
		if ( start_free == 0){
			// heap空间不足,malloc()失败
			obj * volatile * my_free_list, *p;
			// 试着检视我们手上拥有的东西,这不会造成伤害,我们不打算尝试配置
			// 较小的区块,因为那在多进程机器上容易导致灾难
			// 以下搜寻适当的free_list
			// 所谓适当是指“尚有未用区块,且区块够大”之free_list
			for (int i = size; i <= __MAX_BYTES; i += __ALIGN){
				my_free_list = free_list + FREELIST_INDEX(i);
				p = *my_free_list;
				if ( p != 0){    // free_list内尚有未用区块
					// 调整free_list以释放未用区块
					*my_free_list = p->free_list_link;
					start_free = (char *) p;
					end_free = start_free + i;
					// 递归调用自己,为了修正nobjs
					return chunk_alloc(size, nobjs);
					// 注意:任何残余零头中将被编入适当的free_list备用
				}
			}
			end_free = 0;   // 如果出现意外(到处都没有可用内存了)
			// 调用一级配置器,看看out-of-memory机制是否能尽点力
			start_free = (char*) malloc_alloc::allocate(bytes_to_get);
			// 这会导致抛出异常,或内存不足的情况获得改善
		} // endif
		heap_size += bytes_to_get;     // 初始的时候这个heap_size是多少?如何确定?初始为0
		end_free = start_free + bytes_to_get;
		// 递归调用自己,为了修正nobjs
		return chunk_alloc(size, nobjs);
	}
} 

chunk_alloc处理流程图

STL源码解析阅读理解:Allocator_第4张图片

你可能感兴趣的:(C++)