身为C++标准库最重要的组成部分,STL(标准模板库)不仅是一个可复用组件库,而且是一个包罗万象算法与数据结构的软件框架.STL实现版本多样,而SGI版本不论在技术层次,源代码组织,源代码可读性上,均有卓越的表现.
以STL的运用角度而言,空间配置器是最不需要介绍的东西,它总是隐藏在一切组件的背后,默默工作.但从STL的实现角度而言,空间配置器又是极其重要的,因为整个STL的操作对象都放在容器之内,而容器一定需要配置空间以置放资料.
并且阅读和剖析名家代码是提高编程水平的捷径.源码之前,了无秘密.大师们的缜密思维,经验结晶,技术思路,独到风格,都原原本本体现在源码之中.在你仔细推敲之中,迷惑不解之时,恍然大悟之际,你的经验,思维,视野,知识乃至技术品味都会获得快速的成长…
SGI设计了双层级配置器,第一级配置器直接使用malloc()和free(),第二级配置器则是情况采用不同的策略:当配置区块超过128bytes时,视之为"过小",为了降低额外负担,便采用复杂的内存池(memory pool)整理方式,而不再求助于第一级配置器. 一二级配置器的关系,及实际运用方式,见下图:
第一级配置器以malloc(),free(),realloc()等C函数执行实际的内存配置,释放,重新配置操作,并实现出类似C++ new_handler的机制.它不能直接运用C++ new_handler机制,因为它并非使用::operator new 来配置内存.所谓C++ new handler机制是,你可以要求系统在内配置需求无法被满足时,调用一个你所指定的函数.换句话说,一旦::operator new 无法完成任务,在丢出std::bad_alloc异常状态之前,会先调用由客端指定的处理例程.该例程通常即被称为new_handler. new_handler 解决内存不足的做法有特定的模式,可参考<<Effective C++>>2e 条款7.
SGI第一级配置器的allocate()和realloc()都是在调用malloc和realloc()不成功后,该调用oom_malloc()和oom_realloc().后两者都有内循环,不断调用"内存不足处理例程",期望在某次调用之后,获得足够的内存而圆满完成任务.但如果"内存不足处理例程”并未被客端设定,oom_malloc()和oom_realloc()便调用__THROW_BAD_ALLOC, 丢出bad_alloc异常信息,或利用exit(1)硬生生中止程序.
第二级配置器多了一些机制,避免太多小额区块造成内存的碎片.小额区块带来的其实不仅是内存碎片,配置时的额外负担也是一个大问题.额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存.若区块越小,额外负担所占的比例就越大,越显得浪费.
SGI第二级配置器的做法是,如果区块够大,超过128bytes时,就移交第一级配置器处理.当区块小于128bytes时,则以内存池(memory pool)管理.此法又称为次层配置:每次配置一大块内存,并维护对应之自由链表(free_list).下次若再有相同大小的内存需求,就直接从free_lists中拨出.如果客端释还小额区块,就有配置器回收到free_lists中(配置器除了负责配置,也负责回收).为了方便管理,SGI第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30bytes,就自动调整为32bytes), 并维护16个free_lists,各自管理大小分别为8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128bytes的小额区块.
当调用allocate()函数申请空间时,它会从free_list中寻找内存,当free_list中没有可用区块时,就调用refill(),准备为free_list重新填充空间,新的空间将取自内存池(经由chunk_alloc()完成).refill函数会接收一个额外的返回值(传引用的nobjs), 函数根据这个值做出不同的响应.
从内存池中取空间给free_list使用是chunk_alloc()函数的工作: chunk_alloc()函数以end_free - start_free来判断内存池的水量.如果水量充足,就直接调出20个区块返回给free_list.如果水量不足以提供20个区块,但还足够供应一个以上的区块,就拨出这不足20个区块的空间出去.这时候器传引用的njobs参数将被修改为实际能供应的区块数.如果内存池连一个区块空间都无法供应,对客端显然无法交待,此时便需要利用malloc()从heap中配置内存,为内存池注入活水源头以应付需求.新水量的大小为需求量的两倍,再加上一个随着配置次数增加而越来越大的附加量.
举个例子,假设程序一开始,客端就调用chunk_alloc(32,20),于是malloc()配置40个32bytes区块,其中第一个交出,另19个交给free_list[3]维护,余20个留给内存池.接下来客端调用chunk_alloc(64, 20),此时free_list[7]空空如也,必须想内存池要求支持.内存池只够供应(32*20)/64=10个64bytes区块,就把这10个区块返回,第一个交给客端,余下9个由free_list[7]维护.此时内存池全空.接下来在调用chunk_alloc(96,20),此时free_list[11]空空如也,必须向内存池要求支持,而内存池此时也是空的,于是以malloc()配置40+n(附加量)个96bytes区块,其中第1个交出,另19个交给free_list[11]维护,余20+n(附加量)个区块留给内存池…
万一山穷水尽,整个system heap空间都不够了(以至于无法为内存池诸如活水源头),malloc()行动失败,chunk_alloc()就四处寻找有无"尚有未用区块,且区块够大"之free_lists.找到了就挖一块交出,找不到调用第一级配置器.第一级配置器其实也是使用malloc()来配置内存,但它有out_of_memory处理机制(类似new_handler机制),或许有机会释放其他的内存拿来此处使用.如果可以,就成功,否则就发出异常.
以上便是整个第二级空间配置器的设计.
stl_alloc.h
#if 1
#include
#include
#include
using namespace std;
//#define __THROW_BAD_ALLOC throw bad_alloc
#define __THROW_BAD_ALLOC cerr<<"Throw bad alloc, Out Of Memory."<
#elif !defined (__THROW_BAD_ALLOC)
#include
#define __THROW_BAD_ALLOC cerr<<"out of memory"<
#endif
//一级配置器
template<int inst>
class __malloc_alloc_template
{
//oom : out of memory
private:
static void* oom_malloc(size_t);
static void* oom_realloc(void *, size_t);
static void(* __malloc_alloc_oom_handler)(); //函数指针,代表的函数将用来处理内存不足的情况
public:
static void* allocate(size_t n)
{
void *result = malloc(n); //第一级配置器直接使用malloc()
//malloc()无法满足需求时,改用 oom_malloc()
if(0 == result)
result = oom_malloc(n);
return result;
}
static void deallocate(void *p, size_t)
{
free(p); //第一级配置器直接使用free()
}
static void* reallocate(void *p, size_t, size_t new_sz)
{
void *result = realloc(p, new_sz); //第一级配置器直接使用realloc()
//realloc()无法满足需求时,改用oom_realloc()
if(0 == result)
oom_realloc(p,new_sz);
return result;
}
public:
//set_new_handler(Out_Of_Memory);
//以下仿真C++的set_new_handler(),可以通过它指定你自己的out_of_memory handler
static void(*set_malloc_handler(void(*f)()))()
{
void(*old)() = __malloc_alloc_oom_handler;
__malloc_alloc_oom_handler = f;
return (old);
}
};
//初值为0,有待客端设定
template<int inst>
void (*__malloc_alloc_template::__malloc_alloc_oom_handler)() = 0;
template<int inst>
void* __malloc_alloc_template::oom_malloc(size_t n)
{
void *result;
void(* my_malloc_handler)();
for(;;) //不断尝试释放,配置,再释放,再配置...
{
my_malloc_handler = __malloc_alloc_oom_handler;
if(0 == my_malloc_handler)
{
__THROW_BAD_ALLOC;
}
(*my_malloc_handler)(); //调用处理程序,企图释放内存
result = malloc(n); //再次尝试配置内存
if(result)
return result;
}
}
template<int inst>
void* __malloc_alloc_template::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;
/////////////////////////////////////////////////////////////////////////////////////
//二级配置器
enum {__ALIGN = 8}; //小型区块的上调边界
enum {__MAX_BYTES = 128}; //小型区块的上限
enum {__NFREELISTS = __MAX_BYTES / __ALIGN}; //free_lists个数
template<bool threads, int inst>
class __default_alloc_template
{
public:
static void* allocate(size_t n);
static void deallocate(void *p, size_t n);
static void* reallocate(void *p, size_t, size_t new_sz);
private:
static size_t ROUND_UP(size_t bytes)
{//将bytes上调至8的倍数
return (((bytes) + __ALIGN-1) & ~(__ALIGN-1));
}
private:
//free_lists的节点结构
union obj
{
union obj * free_list_link;
char client_data[1];
};
private:
static obj* volatile free_list[__NFREELISTS]; //16个free_lists
static size_t FREELIST_INDEX(size_t bytes)
{//根据区块大小,决定使用第n号的free_lists
return ((bytes)+__ALIGN-1) / __ALIGN - 1;
}
private:
static char *start_free; //内存池起始位置,只在chunk_alloc()中变化
static char *end_free; //内存池结束位置,只在chunk_alloc()中变化
static size_t heap_size;
static void *refill(size_t n); //返回一个大小为n的对象,并可能加入大小为n的其它区块到free_list
static char* chunk_alloc(size_t size, int &nobjs); //配置一大块空间,可容纳njobs个大小为"size"的区块
};
//以下是static data member的定义与初始值设定
template<bool threads, int inst>
char* __default_alloc_template::start_free = 0;
template<bool threads, int inst>
char* __default_alloc_template::end_free = 0;
template<bool threads, int inst>
size_t __default_alloc_template::heap_size = 0;
template<bool threads, int inst>
typename __default_alloc_template::obj* volatile
__default_alloc_template::free_list[__NFREELISTS] =
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
template<bool threads, int inst>
void* __default_alloc_template::allocate(size_t n)
{
obj * volatile *my_free_list;
obj *result;
//大于128就调用第一级配置器
if(n > __MAX_BYTES)
{
return malloc_alloc::allocate(n);
}
my_free_list = free_list + FREELIST_INDEX(n); //寻找16个free_lists中合适的一个
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
return result;
}
template<bool threads, int inst>
void* __default_alloc_template::refill(size_t n)
{
int nobjs = 20; //申请20*2个块,一半挂在自由链表,一半用作内存池(20为一个经验值)
char *chunk = chunk_alloc(n, nobjs); //调用chunk_alloc(),尝试取得nobjs个区块作为free_list的新节点(njobs为传引用)
obj * volatile *my_free_list; //my_free_list为二级指针,volatile为一个类型修饰符,与线程相关
obj *result;
obj *current_obj, *next_obj;
int i;
if(1 == nobjs) //如果只获得一个区块,这个区块就分配给调用者,free_list无新节点
return chunk;
//否则准备调整free_list,并纳入新节点
my_free_list = free_list + FREELIST_INDEX(n);
result = (obj*)chunk; //这一块准备返回给客端
//以下引导free_list指向新配置的空间(取自内存池)
*my_free_list = next_obj = (obj*)(chunk+n);
//以下将free_list的各节点串接起来
for(i=1; ; ++i) //从1开始,第0个将返回给客端
{
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);
}
//注意nobjs传引用,假设size已经上调至8的倍数
template<bool threads, int inst>
char* __default_alloc_template::chunk_alloc(size_t size, int &nobjs)
{
char *result;
size_t total_bytes = size * nobjs;
size_t bytes_left = end_free - start_free; //内存池剩余空间
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;
}
else
{//内存池剩余空间连一个区块的大小都无法提供
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
//以下尝试这让内存池中的残余零头还有利用价值
if(bytes_left > 0)
{
//内存池内还有一些零头,先配给适当的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;
}
//配置heap空间,用来补充内存池
start_free = (char *)malloc(bytes_to_get);
if(0 == start_free)
{
int i;
obj * volatile *my_free_list, *p;
//试着检视我们手上拥有的东西,这不会造成伤害,我们不打算尝试配置较小的区块,因为那在多进程机器上容易造成灾难,以下搜寻适当的free_list,所谓适当是指"尚有未用区块,且区块够大"之free_list
for(i=size; i<=__MAX_BYTES; i += __ALIGN)
{
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if(0 != p)
{//free_list内尚有未用区块,调整free_list以释放未用区块
*my_free_list = p->free_list_link;
start_free = (char *)p;
end_free = start_free + i;
return chunk_alloc(size, nobjs); //递归调用自己,为了修正nobjs
}
}
//如果出现意外(山穷水尽,到处都没内存可用了),调用第一级配置器,看看out_of_memory机制能否尽点力
end_free = 0;
start_free = (char *)malloc_alloc::allocate(bytes_to_get); //这会导致抛出异常,或内存不足的情况得到改善
}
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return chunk_alloc(size, nobjs); //递归调用自己,为了修正nobjs
}
}
template<bool threads, int inst>
void __default_alloc_template::deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * volatile * my_free_list;
if (n > (size_t) __MAX_BYTES)
{//大于128就调用第一级配置器
malloc_alloc::deallocate(p, n);
return;
}
my_free_list = free_list + FREELIST_INDEX(n); //寻找对应的free_list
//调整free_list,回收区块
q->free_list_link = *my_free_list;
*my_free_list = q;
}
memory
#include"stl_alloc.h"
test.cpp
#include
#include
#include "memory"
using namespace std;
void Out_Of_Memory()
{
cout<<"Out Of Memory."<exit(1);
}
int main()
{
void (*pfun)() = __malloc_alloc_template<0>::set_malloc_handler(Out_Of_Memory);//语句1
int *p = (int*)__malloc_alloc_template<0>::allocate(sizeof(int) * 2073741824);
if(p == NULL)
{
cout<<"Error."<exit(1);
}
cout<<"OK"<return 0;
}
test2.cpp
#include
#include
#include "memory"
using namespace std;
int main()
{
char *p1 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 32);
char *p2 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 32);
char *p3 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 64);
char *p4 = (char *)__default_alloc_template<0,0>::allocate(sizeof(char) * 96);
return 0;
}
test.cpp 运行结果如下:
不加语句1, 调用默认的处理程序
加上语句1,调用了Out_Of_Memory()处理程序
test2.cpp:
这个测试没有输出结果,但每句可能会走不同的代码分支,可以根据之前介绍chunk_allco()函数时的例子,用单步调试的方法进行跟踪,就能够了解空间配置器是如何配置空间了.