前言
这本书去年就有看过一点,但是那个时候读源码读的太痛苦,遂放弃了一段时间,后来断断续续又看了点,但是每次碰到allocator部分的知识依旧是云里雾里。现在决定从头开始再看一遍。
以下内容是我阅读 侯捷《STL源码剖析》 这本书的笔记。如果有理解有误的地方还望各位大佬能够不吝指出。
在我们正式开始介绍Allcoator(空间配置器)之前先想一个问题。我们为什么要有空间配置器?这个问题我知道很简单,顾名思义,当然是为了分配空间啦。那分配空间的目的能?那当然是为了存储数据啦。就像书中所言” 整个STL的操作对象(所有数值)都存放在容器之内,而容器一定需要配置空间以存放资料 “
这里将allocator定义为空间配置器而不是内存配置器的原因:因为空间不一定是内存,空间也可以是磁盘或其它辅助存储介质。【书中原话】
SGI的空间配置器有两种:
它们两者之间的区别:
我们平时调用的new/delete实际上就是::operator new和::operator delete这两个函数。
我们来看下面的语句:
A *a = new A; // 分配内存,然后构造对象
delete a; // 将对象析构,然后释放内存
这个语句等号右边的部分(new A)中实际上进行了两个操作:
同理,对于delete a;而言也包含两个操作
这也就为什么我们说在std::allocator中,内存分配和对象构造是被封装在同一个函数中的原因了。因为它的allocate函数的底层实现就是通过operator new来完成的,人家operator new本身就包含了内存分配和对象构造啦,你总不能把人家强行扯开吧~
下面分别介绍std::allocator和std::alloc。
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;
}
template<class T>
inline void deallocate(T* buffer){
::opertator delete(buffer); // 直接调用delete析构对象,并释放内存
}
该配置器中对象的构造与析构放在头文件:#incldue
该配置器中空间的配置与释放放在头文件:#incldue
该配置器的空间分配又分为两种:
需要用到头文件 #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()
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设计双层配置器的原因: 考虑到小型区块可能造成的内存破碎问题,第一季配置器直接使用malloc()和free(),第二级配置器则视情况采用不同的策略。
一级空间配置器的特点:
接下来看看一级空间配置器是如何分配内存和释放内存,以及如何处理内存不足的情况的。
代码实现如下:
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(),这个后面会介绍。
这部分的内容很少,也很简单,分配成功就返回内存地址,分配失败就调用处理失败的函数。
代码如下:
static void deallocate(void *p, size_t){
free(p); // 一级配置器直接使用free
}
这部分代码没啥好说的,就是直接使用free将分配的空间进行释放就行。
代码如下:
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();
代码如下:
static void (*set_malloc_handler(void(*f)()))(){
// 函数的返回值是一个函数的地址
void (* old) () = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return (old);
}
代码分析:
这个代码是用来处理内存不足的情况的,在oom_malloc和oom_realloc中被循环调用。
代码如下:
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)强行终止程序。
代码如下:
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
二级空间配置器的做法:
二级空间配置器的特点:
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 |
---|
每个free_list[i]都指向一个free_list的节点。
根据前面介绍过的二级空间配置器的做法我们可用设计一个相关的allocate空间分配函数。
allocate:()
步骤:
- 判断区块大小,>128bytes就调用一级配置器
- <=128bytes就检查对应的free list;
- 如果free list之内有可用的区块,就直接拿来用
- 如果没有就将区块大小调至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()
步骤:
- 判断区块大小;
- 释放区块>128bytes调用第一级配置器;
- <=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()
描述:
- 默认申请的区块数量是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);
}
}