最近学习侯捷大师的【STL源码剖析】,写博客来总结一下
侯捷大师在该书中剖析的是SGI版本的STL
开始介绍本篇博客的重点:空间配置器
C++的内存配置操作是由new 和delete来操作的
class Foo{...};
Foo *pf = new Foo;
delete pf;
其中new算式中包含两段操作:
(1)调用::operator new配置空间
(2)调用对象的构造函数Foo::Foo()来构造对象
同样delete算式也包含两段操作。
为了精密分工,STL allocator 将分配空间和对象构造的两段操作分开,内存配置操作由 allocate() 和 deallocate() 负责,对象构造由construct()和destroy()负责。
内存配置函数在头文件 stl_alloc.h
对象构造函数在头文件 stl_construct.h
本文主要介绍的内存配置
SGI版本的STL的空间配置器有三个版本:标准版本(不推荐使用),一级空间配置器,二级空间配置器
虽然标准版本不推荐使用,但是还是介绍一下。
SGI标准的空间配置,std::allocator
直接看代码,allocator 和 deallocator的实现
template <class T>
inline T* allocate(ptrdiff_t size, T*)
{
set_new_handler(0);
T* tmp = (T*)(::opeartor new((size_t)(size * sizeof(T))));
if(tmp == 0)
{
ceer << " out of memory" << endl;
exit(1);
}
return tmp;
}
template<class T>
inline void dellocate(T* buffer)
{
::opeartor delete(buffer);
}
从代码中可以看出,SGI标准版效率不佳呗,只是把C++的 ::opeartor new 和 :: opeartor delete做了一层薄薄的包装。
开始介绍本文的重点,一级空间配置器malloc_alloc_template和二级空间配置器 default_alloc_template
先来看看他们的关系
从上图可以看出 一级空间配置器 使用的是malloc 和free 函数来管理
二级空间配置器是维护16个自由链表,如果大于128字节就调用一级空间配置器
从上图可以看出
SGI 并没有定义_USE_MALLOC,所以将alloc定义为二级空间配置器
SGI为了使Alloc标准化,对alloc进行包装 simple_alloc。
SGI一级空间配置器:malloc_alloc_template
第一级空间配置器以malloc(), free(), realloc() 等C函数执行实际的内存配置,释放,再配置,并实现出类似C++ new-handler的机制,
(因为不是使用的::opeartor new来配置内存,所以无法直接使用c++的 new-handler机制)
什么是C++的new-handler机制:就是要求系统在内存配置无法满足时调用一个你所指定的函数。可以参考Effective C++3rd 条款49
SGI第一级空间配置器的allocate() 和 realloc()都是在调用malloc和realloc不成功后调用oom_malloc和 oom_realloc。
oom_malloc和 oom_realloc函数都有内循环,不断调”内存不足处理例程’,期望某次调用之后,获得足够的内存而圆满完成任务,但如果“内存不足处理例程”并未被客户端设定,oom_malloc和 oom_realloc便不客气的调用_THROW_BAD_ALLOC,丢出bad_alloc异常信息,利用exit(1)终止进程。
template<int inst>
void* Malloc_alloc_template<inst>::Oom_Malloc(size_t n)
{
void(*my_malloc_handler)();
void *res;
//不断尝试释放,配置,
for (;;)
{
my_malloc_handler = _Malloc_alloc_oom_handle;
if (0 == my_malloc_handler)
throw std::bad_alloc();
//尝试释放
(*my_malloc_handler)();
res = malloc(n);
if (res != 0)
return res;
}
}
template<int inst>
void* Malloc_alloc_template<inst>::Oom_Realloc(void* p, size_t n)
{
void(*my_malloc_handler)();
void res;
for (;;)
{
my_malloc_handler = _Malloc_alloc_oom_handle;
if (0 == my_malloc_handler)
throw bad_alloc;
(*my_malloc_handler)();
res = realloc(p, n);
if (res)
return res;
}
}
以下给出模拟实现一级空间配置器,应该不是特别难理解
template<int inst>
class Malloc_alloc_template
{
typedef void(*Fun_pointer)(void);
public:
static void* Allocate(size_t n)
{
void *res = malloc(n);
if (res == 0)
res = Oom_Malloc(n);
return res;
}
static void Deallocate(void * p, size_t /* __n */)
{
free(p);
}
static void* Reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void *res = realloc(p, new_sz);
if (0 == res)
res = Oom_Realloc(p, new_sz);
return res;
}
//仿真c++的set_new_handler(),指定自己的out of memory handler()
// static void (* Set_malloc_handler( void(*f)() ))()
// 参数为函数指针,返回值为参数指针的函数
static Fun_pointer Set_malloc_handler(Fun_pointer f)
{
void(*old)() = _Malloc_alloc_oom_handle;
_Malloc_alloc_oom_handle = f;
return old;
}
private:
//以下函数处理内存不足
static void* Oom_Malloc(size_t);
static void* Oom_Realloc(void *, size_t);
static void(*_Malloc_alloc_oom_handle)();
};
template<int inst>
void(*Malloc_alloc_template<inst>::_Malloc_alloc_oom_handle)() = 0;
//二级空间配置调用
typedef Malloc_alloc_template<0> Malloc_alloc;
接下来就介绍本文的重中之重
SGI二级空间配置器:Default_alloc_template
SGI二级空间配置器的做法:如果区块够大,超过128字节,就移交给第一级配置器处理。区块小于128字节时则以内存池管理,此法又称为次级配置:该配置器维护16个free_list, 各自管理大小分别为8,16,24,32,40,….128字节的小块区块,用户申请大小为n = 30的空间 ,先将n提升到8的倍数32,定位到32那个free_list,看该list下是否用空间,没有的话,从内存池中取出空间,如果内存池也没有空间,则需要从系统内存中申请空间。
该配置器维护的16个free_list,其节点结构:
union obj
{
union obj* free_list_link;//指向下个obj对象
char client_data[1];//用户数据
}
注意:上述obj所用的是union,从第一个字段看出指向相同形式的另一个obj,从第二个字段可被视为一个指针,指向实际区块。这样一物两用不会造成为了维护链表所必须的指针而造成的另一种浪费。
给出整体代码:
enum { _ALIGN = 8 };
enum { _MAX_BYTES = 128 };
enum { _NFREELISTS = 16 };
template<int inst>
class Default_alloc_template
{
private:
//free_list的节点结构
union Obj
{
union Obj *free_list_link;//指向另一个Obj对象
char client_data[1];
};
//配置器维护的16个free_list
static Obj* volatile free_list[_NFREELISTS];
//将byte上调至8的倍数
static size_t Round_up(size_t bytes)
{
//return (bytes + 7) / 8) * 8;
return (((bytes)+_ALIGN - 1)&~(_ALIGN - 1));
}
//根据区块大小,获取free_list的下标
static size_t FreeList_Index(size_t bytes)
{
//return (bytes -1)/8
return ((bytes + _ALIGN - 1) / _ALIGN - 1);
}
//没有可用的free_list,调用Chunk_alloc从内存池中分配重新填充free_list,
static void* Refill(size_t n);
//从内存池中取出大块空间,可容纳nobjs个大小为size的区块
//参数nobjs为引用,输入输出参数
static char* Chunk_alloc(size_t size, int& nobjs);
//内存池的大小
static char* start_free;
static char* end_free;
static size_t heap_size; //用来在heap上申请空间的附加量(随着配置次数的增加而愈来愈大),在函数chunk_alloc中向系统申请空间使用
public:
//空间配置函数
static void* Allocate(size_t n);
//空间释放函数
static void Deallocate(void *p, size_t n);
//空间再配置函数
static void * Reallocate(void *p, size_t old_sz, size_t new_sz);
};
template<int inst>
typename Default_alloc_template<inst>::Obj* volatile Default_alloc_template<inst>::free_list[_NFREELISTS] =
{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
template<int inst>
char * Default_alloc_template<inst>::start_free = 0;
template<int inst>
char *Default_alloc_template<inst>::end_free = 0;
template<int inst>
size_t Default_alloc_template<inst>::heap_size = 0;
空间配置函数:allocate
该函数首先判断区块的大小,大于128字节就调用第一级空间配置器,小于128字节就检查对应的free_list。如果free_list之内有可以用的区块,就直接拿来使用,如果没有可用的区块,就将区块大小上调至8的倍数,然后调用refill()函数,为free_list重新填充空间, refill()稍后介绍。
static void* Allocate(size_t n)
{
//1 调用一级空间配置器
if (n > (size_t)_MAX_BYTES)
return Malloc_alloc::Allocate(n);
Obj * volatile *my_freelist;
Obj *res;
//2 找到对应的free_list
my_freelist = free_list + FreeList_Index(n);
res = *my_freelist;
if (res == 0)
{
//没有找到可用的free_list,准备重新填充free_list
void* r = Refill(FreeList_Index(n));
return r;
}
//3、从freelist取下分配的区块,调整free list
*my_freelist = (Obj*)res->free_list_link;
return res;
}
空间释放函数:deallocate()
直接看代码,应该可以看懂
static void Deallocate(void *p, size_t n)
{
if (n > (size_t)_MAX_BYTES)
{
Malloc_alloc::Deallocate(p, n);
return;
}
Obj* volatile * my_free_list;
Obj* del = (Obj*)p;
//寻找对应的free_list
my_free_list = free_list + FreeList_Index(n);
//头插,回收区块
(Obj*)del->free_list_link = *my_free_list;
*my_free_list = del;
}
空间再配置函数:reallocate()
static void * Reallocate(void *p, size_t old_sz, size_t new_sz)
{
//如果都大于128
if (old_sz > (size_t)_MAX_BYTES && new_sz > (size_t)_MAX_BYTES)
{
return (realloc(p, new_sz));
}
//不需要重新分配内存
if (FreeList_Index(old_sz) == FreeList_Index(new_sz))
return p;
//需要重新分配内存,调用空间配置函数allocate
void *res = Allocate(new_sz);
//将原空间的内容拷贝到新的空间
size_t copy_sz = new_sz > old_sz ? old_sz : new_sz;
memcpy(res, p, copy_sz);
//释放之前的空间
Deallocate(p, old_sz);
return res;
}
重新填充free_list函数:refill()
refill函数:发现free_list中没有可用区块了时,调用refill函数,准备为free_list重新填充空间,新的空间取自内存池(由chunk_alloc()函数完成)。
函数缺省取得20个新区块,
template<int inst>
void *Default_alloc_template<inst>::Refill(size_t n)
{
int nObjs = 20;
char * chunk = Chunk_alloc(n, nObjs);
Obj * res;
if (1 == nObjs)
return (chunk);
//将剩余的区块挂接到free_list
Obj* volatile* my_free_list = free_list + FreeList_Index(n);
res = (Obj *)chunk;
Obj* next_obj = *my_free_list = (Obj*)(chunk + n);
Obj* cur_obj;
for (int i = 1;; ++i)
{
cur_obj = next_obj;
next_obj = (Obj*)((char*)next_obj + n);
if (nObjs - 1 == i)
{
cur_obj->free_list_link = 0;
break;
}
else
cur_obj->free_list_link = next_obj;
}
/*
//采用头插法:不需要末尾置0,第一次插入已经置为空了
Obj* cur_obj =(Obj*)(chunk + n);
for (int i = 1; i < nObjs; ++i)
{
cur_obj->free_list_link = *my_free_list;
*my_free_list = cur_obj;
cur_obj = (Obj*)((char *)chunk + n*i);
}
*/
return res;
}
内存池函数:chunk_alloc()
chunk_alloc()函数:从内存池中取空间给free_list使用,
分三种情况:
n已经上调至8的倍数,需要申请的区块大小
(1) 内存池剩余空间完全满足需求量:left_bytes >=20*n
(2) 内存池剩余空间不能完全满足,但是大于一个区块:left_bytes >= n
(3) 内存池剩余空间连一个区块都不能提供:
(3.1)先将内存池残余的零头配置给适当的free_list.
(3.2)malloc 从堆申请 2*20*n多的空间 ,多(heap_size>>4)
(3.2.1)malloc失败,检测n之后的free_list,尚未使用,且足够大的空间,调整free_list 释放出未用区块
(3.2.2)山穷水尽,没有可用,调用一级配置器allocate,看out-of-memory机制能否尽力
//从内存池分配区块,如果不够从内存中分配内存添加到内存池中
template<int inst>
char *Default_alloc_template<inst>::Chunk_alloc(size_t size, int& nobjs)
{
size_t total_bytes = size * nobjs;
size_t left_bytes = end_free - start_free;//内存池剩余的空间
char * res;
//内存池剩余的空间大于需要申请的空间
if (left_bytes >= total_bytes)
{
res = start_free;
start_free += total_bytes;
return res;
}
//内存池不够20个size,但是大于等于1个
else if (left_bytes >= size)
{
nobjs = left_bytes / size;
total_bytes = size * nobjs;
res = start_free;
start_free += total_bytes;
return res;
}
//内存池不够一个size
else
{
//内存池还有一个零头,将其挂在freelist
if (left_bytes > 0)
{
Obj* volatile * my_free_list = free_list + FreeList_Index(left_bytes);
((Obj*)start_free)->free_list_link = *my_free_list;
*my_free_list = (Obj*)start_free;
}
size_t get_new_bytes = 2 * total_bytes + Round_up(heap_size >> 4);//除以16
//向系统配置空间,补充内存池
start_free = (char *)malloc(get_new_bytes);
if (0 == start_free)
{
//系统也没有空间,释放未使用,且区块比较大的区域
Obj* volatile * my_free_list;
Obj* p;
//??????????????为什么从size开始
for (int i = size; i < _MAX_BYTES; i += _ALIGN)
{
my_free_list = free_list + FreeList_Index(i);
p = *my_free_list;
//freelist 有未使用的大区块
if (0 != p)
{
*my_free_list = p->free_list_link;
start_free = (char *)p;
end_free = start_free + i;
return (Chunk_alloc(size, nobjs));
}
}
//free_list 也没有空间
end_free = 0; //置0,防止使用了不是存在的空间
//调用一级配置器,看看out of memory机制能否尽点力
start_free = (char *)Malloc_alloc::Allocate(get_new_bytes);
//Oom_Malloc可能会抛出异常
}
heap_size += get_new_bytes;
end_free = start_free + get_new_bytes;
return (Chunk_alloc(size, nobjs));
}
}
封装后的空间配置器:simple_alloc
template<class T, class Alloc = alloc>
class Simple_alloc
{
public:
static T* Allocate(size_t n)
{
return 0 == n ? 0 : (T*)Alloc::Allocate(n* sizeof(T));
}
static T*allocate()
{
return (T*)Alloc::Allocate(n* sizeof(T));
}
static void Deallocate(T* p, size_t n)
{
if (0 != n)
Alloc::Deallocate(p, n*sizeof(T));
}
static void Deallocate(T* p)
{
Alloc::Deallocate(p, sizeof(T));
}
};
总结一下,STL对内存的请求与释放
STL考虑到小型内存区块的碎片问题,设计了两级配置器,第一级配置器直接使用malloc和free,第二级配置器根据申请的空间的大小而采用不同的方法,当申请的空间大于128字节时,直接调用调用第一级配置器,当申请的空间小于128字节时,使用一个memory pool的实现机制。
SGI中默认使用的是第二级空间配置器。
第二级空间配置器的实现机制:该配置器维护16个free_list,各自管理8,16,24,32,40….128字节的小额区块,当有这样的配置需求时,将申请的空间提升至8的倍数,定位到对应的free_list,直接从free_list取出一块内存,(如果该free_list为空的话,调用refill函数,重新填充free_list,而refill函数调用chunk_alloc函数从内存池中申请空间)当客户端归还内存时,根据归还内存的大小,将内存插入到对应的free_list上。
STL中的内存分配器实际上是基于空闲列表(free list)的分配策略,最主要的特点是通过组织16个空闲列表,对小对象的分配做了优化。
1)小对象的快速分配和释放。当一次性预先分配好一块固定大小的内存池后,对小于128字节的小块内存分配和释放的操作只是一些基本的指针操作,相比于直接调用malloc/free,开销小。
2)避免了内存碎片的产生。
释放:
大于128的内存,客户程序Deallocate之后会调free释放掉,归还给了系统。
小于128的内存,程序中不曾释放,只是在自由链表中,且配置器的所有方法,成员都是静态的,那么他们就是存放在静态区。释放时机就是程序结束。