最近拜读了侯捷的《STL源码剖析》一书的前三章内容,很是受教,遂以博客记录之,愿与大家交流分享。碍于鄙人才疏学浅,加之目前水平有限,若有差错,还望不吝赐教。
SGI空间配置器
空间配置器作为STL中最默默无闻的组件,一直在无私的对容器的内存空间进行分配和管理。
我们平时最常用的C++内存分配和释放操作是这样的:
class Foo
{
...
};
Foo* p = new Foo;
delete p;
这里的new要做三件事情:
1)调用::operator new 分配内存
2)调用Foo::Foo()构造对象
3)返回分配到的内存空间地址
delete会做两件事情:
1)调用Foo::~Foo()析构对象
2)调用::operator delete释放内存
SGI STL空间配置器是把内存空间的分配/释放和对象的构造/析构分开来进行的,alloc::allocate()负责内存的分配,alloc::deallocate()负责内存的释放;对象构造由::construct()负责,对象析构由::destroy()负责。它们分别放于两个不同的头文件中,借书中原图来说明:
下面我们就来重点谈谈空间的分配和释放。
空间的分配和释放
STL的设计哲学是:
1)向system heap申请空间
2)考虑multi-threads
3)考虑内存不足时的解决办法
4)考虑小块内存过多造成的内存碎片问题
若频繁分配小块内存会产生外碎片,外碎片是出于任何已分配区域或页面外部的空闲存储块。这些存储块的总和可以满足当前申请的长度要求,但是由于它们的地址不连续或其他原因,使得系统无法满足当前申请。STL是产生内存碎片的典型例子,不过作为高效率的代表,STL自有解决之道——两级配置器。
__USE_MALLOC是一个宏,它的定义与否决定了是调用一级空间配置器还是二级空间配置器。一级空间配置器直接实验malloc()和free(),二级空间配置器视情况而定:当申请的内存超过128bytes时,直接调用一级空间配置器,当申请的内存小于128bytes时就被视为“小块内存”,采用复杂的memory pool和free list机制处理。simple_alloc是STL封装的一个统一的接口,STL中所有容器都使用这个接口。
一级空间配置器
一级空间配置器是一个叫__malloc_alloc_template的类,它直接调用malloc()、remalloc()、free()执行空间的分配、重分配、释放操作。
template <int inst>
class __malloc_alloc_template
{
private:
//以下都是函数指针,用来处理内存不足的情况
static void *oom_malloc(size_t);
static void *oom_realloc(void *, size_t);
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
static void (* __malloc_alloc_oom_handler)();
#endif
public:
static void * allocate(size_t n)
{
void *result = malloc(n); //直接使用malloc
if (0 == result) //内存申请失败
result = oom_malloc(n);
return result;
}
static void deallocate(void *p, size_t /* n */)
{
free(p);//直接使用free
}
static void * reallocate(void *p, size_t /* old_sz */, size_t new_sz)
{
void * result = realloc(p, new_sz);//直接使用realloc
if (0 == result)
result = oom_realloc(p, new_sz);
return result;
}
//模仿C++的set_new_handler机制
static void (* set_malloc_handler(void (*f)()))()
{
void (* old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return(old);
}
};
所谓set_new_handler机制就是,系统在内存分配需求无法被满足时,在抛出std::bad_alloc异常之前,会调用指定的处理程序,该程序就是new_handler,以企图产生更多 内存资源供使用。产生更多可用内存的原理:在程序启动时分配一个大的内存块,然后在第一次调用new-handler时释 放
// malloc_alloc out-of-memory handling
//初值为0,以待用户设定
#ifndef __STL_STATIC_TEMPLATE_MEMBER_BUG
template <int inst>
void (* __malloc_alloc_template<inst>::__malloc_alloc_oom_handler)() = 0;
#endif
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;
if (0 == my_malloc_handler)
{ __THROW_BAD_ALLOC; } //输出"out of memory"
(*my_malloc_handler)(); //调用处理例程,企图释放内存
result = malloc(n); //再次尝试分配内存
if (result) return(result);
}
}
template <int inst>
void * __malloc_alloc_template<inst>::oom_realloc(void *p, size_t n)
{
void (* my_malloc_handler)();
void *result;
for (;;) {
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);
}
}
typedef __malloc_alloc_template<0> malloc_alloc;
一级配置器在调用malloc()、remalloc()失败后,转而调用oom_malloc()、oom_realloc()函数,这两个函数中都有内循环,不断调用内存不足处理程序,以期望得到足够的内存,若实在山穷水尽没有更多内存或是用户并没有设定内存不足处理程序,oom_malloc()、oom_realloc()便会__THROW_BAD_ALLOC抛出异常。
二级空间配置器
其实频繁分配小块内存不仅会造成内存碎片,还有增加系统的额外负担。当你写了下面的语句:
class AppleTree
{
public:
...
private:
int* _apple
};
AppleTree *t = new AppleTree;
你得到的不会只是AppleTree对象的内存,而是内存块大小的数据+AppleTree对象的内存。
原因是operator new()分配 了多大的内存块operator delete()就要释放多大的内存块,而operator delete()想弄清它要释放的内存有多大,就必须知道当初operator new()分配的内存有多大。有一种常用的方法可以让operator new()来告诉operator delete()当初分配的内存大小是多少,就是在它所返回的内存里预先附带一些额外信息,用来指明被分配的内存块的大小。
对于像AppleTree这样很小的对象来说,这些额外的数据信息会使得分配对象时所需要的的内存的大小翻番,给系统带来额外负担。
二级空间配置器要做的事情是,当申请的区块足够大,大于128bytes(对于现在的系统来说,128bytes也可以算是小块内存了,若有需要,用户可以自定义这个界限),直接交由一级空间配置器处理,若小于128bytes,则以memory pool管理,并维护相应的自由链表。为了方便管理,配置器会把任何小块内存需求提升至8的整数倍(如要求13bytes内存,会自动提升为16bytes),所以16个free list管理的内存块大小如图所示。
memory pool也就是内存池,首先会分配到一大块空间,此时若有线程申请空间的话,内存池就会分配20个相同大小的内存块,把第一块地址返回以供使用,剩下的19块挂到对应大小的自由链表下,下次若再有相同大小的内存需求,直接从自由链表中获取(相当于头删)。如果分配出去的小内存块使用完毕被释放了,回收到对应大小的自由链表下(相当于头插)。
memory pool一次性切20个小内存块给free list,而不是只返回仅需的那一块的原因有二:
1)STL是运行在多线程环境中的,并不只有一个线程需要该相应大小的内存块;
2)一次性切多块的话,free list下挂的内存空间地址是连续的,可提高CPU的运行效率。
有人会怀疑,为了维护free list,每个节点需要存储指向下一个节点的指针,这不是又增加了额外负担吗?但是请注意,free list的节点obj被定义成union,可以节省空间。也就是说,当节点位于free-list时,通过free_list_link指向下一块内存,而当节点取出来分配给用户使用的时候,整个节点的内存空间对于用户而言都是可用的,这样在用户看来,就完全意识不到free_list_link的存在,可以使用整块的内存了。
union obj
{
union obj * free_list_link;
char client_data[1]; // The client sees this.
};
下面是二级空间配置器的部分代码实现:
template <bool threads, int inst>
class __default_alloc_template
{
private:
# ifndef __SUNPRO_CC
enum {__ALIGN = 8}; //内存块的提升边界
enum {__MAX_BYTES = 128}; //内存块的大小上限
enum {__NFREELISTS = __MAX_BYTES/__ALIGN}; //free list的数量,此次为16
# endif
static size_t ROUND_UP(size_t bytes) //内存提升函数
{
return (((bytes) + __ALIGN-1) & ~(__ALIGN - 1)); //得到8的倍数,很高效的技巧
}
__PRIVATE:
union obj //free list的节点结构
{
union obj * free_list_link;
char client_data[1];
};
private:
# ifdef __SUNPRO_CC
static obj * __VOLATILE free_list[];
# else
static obj * __VOLATILE free_list[__NFREELISTS]; //16条free list
static size_t FREELIST_INDEX(size_t bytes) //根据申请的内存大小决定去哪条free list获得提升了的内存,下标从1开始
{
return (((bytes) + __ALIGN-1)/__ALIGN - 1);
}
// 返回大小为n的内存块,并把其余19个挂到free list下
static void *refill(size_t n);
//分配一段nobjs个size大小的大内存,若不够,nobjs会根据实际情况降低
static char *chunk_alloc(size_t size, int &nobjs);
// Chunk allocation state.
static char *start_free; //memory pool起始位置
static char *end_free; //memory pool结束位置
static size_t heap_size; //记录配置器向系统申请的所有内存数量
public:
//空间配置函数allocate()
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;
if (n > (size_t) __MAX_BYTES) //若需要的内存超过128bytes,调用一级空间配置器
{
return(malloc_alloc::allocate(n));
}
my_free_list = free_list + FREELIST_INDEX(n); //小于128bytes就找到对应的free list获取内存
result = *my_free_list;
if (result == 0) //若free list为空,填充内存提升后的free list
{
void *r = refill(ROUND_UP(n));
return r;
}
*my_free_list = result -> free_list_link;//取走free list的第一块内存,调整free list
return (result);
}
//空间释放函数deallocate()
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * __VOLATILE * my_free_list;
if (n > (size_t) __MAX_BYTES) //调用一级空间配置器
{
malloc_alloc::deallocate(p, n);
return;
}
my_free_list = free_list + FREELIST_INDEX(n);
//回收内存块,调整free list
q -> free_list_link = *my_free_list;
*my_free_list = q;
// lock is released here
}
};
有关memory heap和free list的初值设定:
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::start_free = 0;
template <bool threads, int inst>
char *__default_alloc_template<threads, inst>::end_free = 0;
template <bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
template <bool threads, int inst>
__default_alloc_template<threads, inst>::obj * __VOLATILE
__default_alloc_template<threads, inst> ::free_list[
# ifdef __SUNPRO_CC
__NFREELISTS
# else
__default_alloc_template<threads, inst>::__NFREELISTS
# endif
] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, };
二级空间配置器的接口函数
1.从memory pool中取空间供free list使用的函数chunk_alloc()
从memory pool中取nobjs个size大小的空间给free list,若memory pool中有足够的空间,直接分配给free list;若空间不足,把nobjs修改成memory pool此时能提供的size大小空间的个数,并把 nobjs*size大小的空间分配给free list,函数中nobjs传的是引用,可以修改;若memory pool中连一个size大小的空间都分配不出来,则把剩余的内存挂到对应大小的free list下,然后向系统申请内存,需要申请的内存大小为2 * total_bytes + ROUND_UP(heap_size >> 4)。
若memory pool申请内存失败,不能去找比size小的free list要内存,因为那样可能会对多进程的机器造成伤害,此时要去向比size大的free list中收集未被使用的内存,并分配给free list。若实在山穷水尽了,即调用一级空间配置器,一级空间配置器虽然也是用malloc()分配内存,但它有set_malloc_handler处理机制,或许有机会释放其他地方的内存拿来此处使用,如果可以,就成功,否则抛出bad_alloc异常。
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; //memory pool剩余空间
//若memory pool剩余空间满足需求
if (bytes_left >= total_bytes)
{
result = start_free;
start_free += total_bytes;
return(result);
}
//若不能完全满足需求
else if (bytes_left >= size)
{
nobjs = bytes_left/size;
total_bytes = size * nobjs;
result = start_free;
start_free += total_bytes;
return(result);
}
//若连一个size的空间都没有
else
{
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
//充分利用memory pool中剩余空间给对应的free list
if (bytes_left > 0) {
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;
}
//申请堆上的空间,填充内存池
start_free = (char *)malloc(bytes_to_get);
//内存不足,malloc失败
if (0 == start_free)
{
int i;
obj * __VOLATILE * my_free_list, *p;
//去比size大的free list中收集未被使用的空间
for (i = size; i <= __MAX_BYTES; i += __ALIGN) {
my_free_list = free_list + FREELIST_INDEX(i);
//free list中有未用空间
if (0 != p)
{
//调整free list,释放出未用空间给memory pool
*my_free_list = p -> free_list_link;
start_free = (char *)p;
end_free = start_free + i;
//递归调用自己,以修正nobjs
return(chunk_alloc(size, nobjs));
}
}
end_free = 0; //防止异常
//若山穷水尽,调用一级空间配置器
start_free = (char *)malloc_alloc::allocate(bytes_to_get);
}
//调整heap_size
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
//递归调用自己,以修正nobjs
return(chunk_alloc(size, nobjs));
}
}
2.重新填充free list的函数refill()
调用chunk_alloc得到memory pool给自己(当前需要填充的free list)分配的空间,若只有一个大小为n的空间,则直接分配给调用者,free list无新节点,否则把分配到的空间挂到free list下。如果内存池空间不足,分配到的节点数有可能小于20。
template <bool threads, int inst>
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
char * chunk = chunk_alloc(n, nobjs);//memory pool分配给自己的空间
obj * __VOLATILE * my_free_list;
obj * result;
obj * current_obj, * next_obj;
int i;
//若只有一个大小为n的空间
if (1 == nobjs)
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 (i = 1; ; i++) {
current_obj = next_obj;
next_obj = (obj *)((char *)next_obj + n);
if (nobjs - 1 == i) {
current_obj -> free_list_link = 0;
break;
} else {
current_obj -> free_list_link = next_obj;
}
}
return(result);
}
下图便是二级空间配置器的设计(侯捷老师画的图太生动形象了,忍不住想借用)
空间配置器的优缺点
优点
1)解决了外碎片的问题
2)减少了小块内存的申请时间,提高了内存效率
缺点
1)内存提升会造成内碎片
2)小块内存释放之后,并不会立即归还给操作系统,而是放到了自由链表中,会导致系统内存越来越少,除非到程序结束,否则内存不会归还给操作系统。