我在这里枉自揣测一下SGI版本的STL在内存分配时的意图吧,SGI的内存分配器一共有两级,二级分配器用于分配小于等于128字节的内存块,如果申请的内存区块大于128字节,就自动使用一级分配器分配。所以说真正问系统要内存资源的动作全部通过一级分配器,一级分配器是malloc的一个封装,并且强调了在系统内存资源耗尽的时候的处理。记得在Effective C++里面有对内存耗尽时的讨论。那为什么要设计一个二级分配器呢,我想设计者主要有以下几个出发点:
1.提高申请小块内存时的效率,小块内存的使用往往是频繁申请释放,如果每次都使用malloc进行系统调用,未免也太浪费了,影响效率。所以需要每次多申请一些做后备
2.申请小块内存如果每次使用系统调用,每块内存都会有至少4个字节的额外开销用于纪录内存大小(每个编译器的行为有可能不同),这个消耗对程序员来说是不可见的,但是在申请的内存块很小,而数目又很多的情况下就不容忽视了。
3.我们知道无论是简单空闲链表还是内嵌指针的实现中,一个分配器只能服务于某一特定大小的对象。既然二级分配器主要服务于小内存,SGI干脆把128字节以下的内存分成16份,以8个字节递增,为8,16,32,40,48,…120,128分别作一个allocator。于是,SGI STL的空闲链表一共有16个节点,每个节点指向一个用于服务该节点所代表内存大小的空闲链表。
于是二级分配器里面应该有这样的成员:
static const int m_kALIGN = 8;
static const int m_kMAX_BYTES = 128;
static const int m_kN_FREE_LIST = m_kMAX_BYTES/m_kALIGN;
union obj{
union obj* free_list_link;
};
static obj* m_free_list[m_kN_FREE_LIST];
m_free_list的每一个item在一开始都是被初始化成NULL的。
现在我们来考虑一下这样的情景: 申请者要一块大小为28字节的内存,由于28不属于我们预先切好的8的倍数,所以只能先做一个round_up,去试图分配32字节大小的内存 :
static size_t ROUND_UP (size_t bytes)
{
return ( (bytes + m_kALIGN -1) & ~(m_kALIGN -1)); // (28 + 7) & (~7) = 32
}
然后,我们找到32字节所对应的空闲链表的入口:
static size_t FREELIST_INDEX (size_t bytes)
{
return ( bytes + m_kALIGN -1 ) /m_kALIGN -1; //如果bytes=32, (32+8-1)/8 -1 =3,3就是入口位置
}
然后我们找到那个入口,看看对应的空闲链表就位了吗?
obj* result = *(m_free_list + 3);
这个时候result的值是NULL。于是我们从系统中多分配一点,比如说分配个20*32 =640个字节吧,然后的操作就是把32个字节返回给申请着,把剩下的(640-32)字节挂到3这个位置上面。
似乎不错哦,接下来用户又申请一块64字节的内存,哈哈,一样的操作。
现在我们来看一下,整个过程一共出现了2次系统调用。但是SGI的设计者似乎觉得第二次系统调用也是不可以接受的,于是他们在第一次分配的时候分配的是 2 * (20*32)个字节,多出来的20*32=640个字节作为后备,那下一次用户申请64子节的时候:
a)检查64子节对应的空闲链表中有没有
b)如果没有,看看后备区里面有没有
c)如果后备区域也没有了,才问系统要!
1.提高申请小块内存时的效率,小块内存的使用往往是频繁申请释放,如果每次都使用malloc进行系统调用,未免也太浪费了,影响效率。所以需要每次多申请一些做后备
2.申请小块内存如果每次使用系统调用,每块内存都会有至少4个字节的额外开销用于纪录内存大小(每个编译器的行为有可能不同),这个消耗对程序员来说是不可见的,但是在申请的内存块很小,而数目又很多的情况下就不容忽视了。
3.我们知道无论是简单空闲链表还是内嵌指针的实现中,一个分配器只能服务于某一特定大小的对象。既然二级分配器主要服务于小内存,SGI干脆把128字节以下的内存分成16份,以8个字节递增,为8,16,32,40,48,…120,128分别作一个allocator。于是,SGI STL的空闲链表一共有16个节点,每个节点指向一个用于服务该节点所代表内存大小的空闲链表。
于是二级分配器里面应该有这样的成员:
static const int m_kALIGN = 8;
static const int m_kMAX_BYTES = 128;
static const int m_kN_FREE_LIST = m_kMAX_BYTES/m_kALIGN;
union obj{
union obj* free_list_link;
};
static obj* m_free_list[m_kN_FREE_LIST];
m_free_list的每一个item在一开始都是被初始化成NULL的。
现在我们来考虑一下这样的情景: 申请者要一块大小为28字节的内存,由于28不属于我们预先切好的8的倍数,所以只能先做一个round_up,去试图分配32字节大小的内存 :
static size_t ROUND_UP (size_t bytes)
{
return ( (bytes + m_kALIGN -1) & ~(m_kALIGN -1)); // (28 + 7) & (~7) = 32
}
然后,我们找到32字节所对应的空闲链表的入口:
static size_t FREELIST_INDEX (size_t bytes)
{
return ( bytes + m_kALIGN -1 ) /m_kALIGN -1; //如果bytes=32, (32+8-1)/8 -1 =3,3就是入口位置
}
然后我们找到那个入口,看看对应的空闲链表就位了吗?
obj* result = *(m_free_list + 3);
这个时候result的值是NULL。于是我们从系统中多分配一点,比如说分配个20*32 =640个字节吧,然后的操作就是把32个字节返回给申请着,把剩下的(640-32)字节挂到3这个位置上面。
似乎不错哦,接下来用户又申请一块64字节的内存,哈哈,一样的操作。
现在我们来看一下,整个过程一共出现了2次系统调用。但是SGI的设计者似乎觉得第二次系统调用也是不可以接受的,于是他们在第一次分配的时候分配的是 2 * (20*32)个字节,多出来的20*32=640个字节作为后备,那下一次用户申请64子节的时候:
a)检查64子节对应的空闲链表中有没有
b)如果没有,看看后备区里面有没有
c)如果后备区域也没有了,才问系统要!
现在上王道,(不是美女照片-:)),上原代码
static void* allocate (size_t n) //n往往是上层代码sizeof的结果
{
if( n > (size_t)m_kMAX_BYTES ){ // 改用第一级分配器
return ( malloc_alloc::allocate(n));
}
obj** my_free_list = m_free_list + FREELIST_INDEX(n); //找到对应入口
obj* result = *my_free_list;
if( NULL == result){ // 如果对应的节点上没有可用的内存
void* r = refill(ROUND_UP(n)); //去后备区或者系统拿内存,稍后详细讨论
return r;
}
// 如果对应节点上有存货,取一个返回给客户,并且修改指针指向下一个可用内存块
*my_free_list = result->free_list_link;
return result;
}
static void* allocate (size_t n) //n往往是上层代码sizeof的结果
{
if( n > (size_t)m_kMAX_BYTES ){ // 改用第一级分配器
return ( malloc_alloc::allocate(n));
}
obj** my_free_list = m_free_list + FREELIST_INDEX(n); //找到对应入口
obj* result = *my_free_list;
if( NULL == result){ // 如果对应的节点上没有可用的内存
void* r = refill(ROUND_UP(n)); //去后备区或者系统拿内存,稍后详细讨论
return r;
}
// 如果对应节点上有存货,取一个返回给客户,并且修改指针指向下一个可用内存块
*my_free_list = result->free_list_link;
return result;
}
再讲refill之前我们先来想想,从功能上来讲有两步:
a) 返回一个大小为n子节的内存 (其实在内部为了减少系统调用,我们会希望多拿一点,注意了,多拿一点只是良好的愿望,不一定能满足的)
b) 由于会多拿一点,返回n子节内存后,把多拿的内存部门插入到对应的空闲链表头部。
a) 返回一个大小为n子节的内存 (其实在内部为了减少系统调用,我们会希望多拿一点,注意了,多拿一点只是良好的愿望,不一定能满足的)
b) 由于会多拿一点,返回n子节内存后,把多拿的内存部门插入到对应的空闲链表头部。
继续上王道:
void* refill (size_t n) // n在上级函数已经调整到8的倍数了
{
int nobjs = 20; // 虽然只要求一个,但是我希望拿20个,因为既然调用refill了,说明对应的空闲链表上没有存货了
//试图去取20个n大小的内存区域,至于怎么去,交给chunk_alloc去关心吧
char* chunk = chunk_alloc(n, nobjs); // nobjs 是传引用的
if(1==nobjs){ //诶,只拿到一个,也谈不上把多余的内存插入链表了,给申请者吧
return chunk;
}
void* refill (size_t n) // n在上级函数已经调整到8的倍数了
{
int nobjs = 20; // 虽然只要求一个,但是我希望拿20个,因为既然调用refill了,说明对应的空闲链表上没有存货了
//试图去取20个n大小的内存区域,至于怎么去,交给chunk_alloc去关心吧
char* chunk = chunk_alloc(n, nobjs); // nobjs 是传引用的
if(1==nobjs){ //诶,只拿到一个,也谈不上把多余的内存插入链表了,给申请者吧
return chunk;
}
// 接下来把多余的区块加入空闲链表
obj** my_free_list = m_free_list + FREELIST_INDEX(n); //找到入口
obj* result = (obj*)chunk;
//修改表头指针指向下一个可用区域
*my_free_list = (obj*)(chunk + n);
//在chunk内建立free_list,就跟我们前面讲简单空闲链表的操作一样
obj* next_obj = *my_free_list;
for( int i =1; ;++i){
obj* 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;
}
同学们,有点耐心,马上就好了,还剩最后一个chunk_alloc了,不过chunk_alloc还是挺麻烦的哦。我们一起来看看设计chunk_alloc需要考虑的事情:
前面说到,chunk_alloc是被refill调用的,也就是说是在对应的空闲链表上没有存货时被调用,SGI的设计者会优先考虑从后备区域中拿内存。
static char* m_start_free;
static char* m_end_free;
是用来标志后备区的开始和结束的。在程序已开始都被初始化为NULL,也就是一开始后备区里面什么也没有。
obj** my_free_list = m_free_list + FREELIST_INDEX(n); //找到入口
obj* result = (obj*)chunk;
//修改表头指针指向下一个可用区域
*my_free_list = (obj*)(chunk + n);
//在chunk内建立free_list,就跟我们前面讲简单空闲链表的操作一样
obj* next_obj = *my_free_list;
for( int i =1; ;++i){
obj* 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;
}
同学们,有点耐心,马上就好了,还剩最后一个chunk_alloc了,不过chunk_alloc还是挺麻烦的哦。我们一起来看看设计chunk_alloc需要考虑的事情:
前面说到,chunk_alloc是被refill调用的,也就是说是在对应的空闲链表上没有存货时被调用,SGI的设计者会优先考虑从后备区域中拿内存。
static char* m_start_free;
static char* m_end_free;
是用来标志后备区的开始和结束的。在程序已开始都被初始化为NULL,也就是一开始后备区里面什么也没有。
假设程序已开始,用户申请一块32字节的内存,察看对应的空闲链表,发现没有存货,试图使用refill来提取内存,我们的良好愿望是拿20*32字节,把32字节返回,refill调用chunk_alloc来拿这640字节,检查后备区发现祖上什么也没有留下。只能自己伸手问系统要了!
本着每次要都多要一点的指导精神,我们问系统要 2倍的需求,就是 2* 20 * 32 = 2* 640字节。如果能成,我们把640字节中的32字节返回给申请人,余下的640-32字节链入对应的空闲链表。多拿的640字节做后备区使用。
好了,现在用户又来申请一块64字节的内存,察看对应空闲链表,发现没有存货,调用refill,我们指望拿到20*64=1280字节,检查后备区,只有640字节阿,能给多少给多少吧,640字节全给他,相当去640/64 = 10个要求的内存块。把1个返回给客户,把剩下的9个(640-64)字节链入相应的空闲链表。
本着每次要都多要一点的指导精神,我们问系统要 2倍的需求,就是 2* 20 * 32 = 2* 640字节。如果能成,我们把640字节中的32字节返回给申请人,余下的640-32字节链入对应的空闲链表。多拿的640字节做后备区使用。
好了,现在用户又来申请一块64字节的内存,察看对应空闲链表,发现没有存货,调用refill,我们指望拿到20*64=1280字节,检查后备区,只有640字节阿,能给多少给多少吧,640字节全给他,相当去640/64 = 10个要求的内存块。把1个返回给客户,把剩下的9个(640-64)字节链入相应的空闲链表。
先看看chunk_alloc的函数注释:
We allocate memory in large chunks in order to avoid fragmenting the malloc heap too much. We assume that size is properly aligned.
还是上王道-:)
char* chunk_alloc (size_t size, int& nobjs)
{
size_t total_bytes = size* nobjs; // 这里是我们的良好愿望
size_t bytes_left = m_end_free – m_start_free; // 后备区里面剩下的内存
obj* result = NULL; //返回的内存区
if(bytes_left >= total_bytes){ //后备区里面的存货足以满足我们的良好愿望
result = m_start_free;
m_start_free += total_bytes; //修正后备区里剩下的内存量
return result;
}else if(bytes_left >= size){ //后备区足以满足一个(含)以上区块要求
nobjs = bytes_left / size; //改变需求区块数,这是实际能满足的数目
total_bytes = size* nobjs; //改变需求总量
m_start_free += total_bytes;
return result;
}else{ //可怜啊,别说20个了,就算1个也给不起了
//于是准备从系统拿了,要么不开口,开口就要两倍
size_t bytes_to_get = 2* total_bytes + ROUND_UP(m_heap_size >>4 ); // 稍候解释
//这个地方厉害了,先把后备区域中剩下的内存给他链接到相应的空闲链表里面去
if(bytes_left > 0){
obj** my_free_list = m_free_list + FREELIST_INDEX(bytes_left);
//程序能到这里,后备区里面的内存一定只有一个区块,插到链表头部
((obj*)m_start_free)->free_list_link = *my_free_list;
*my_free_list = (obj*)m_start_free;
}
//问系统要
m_start_free = (char*)malloc(bytes_to_get);
if(0==m_start_free){ // 系统没有内存了
//想办法从各个空闲链表里面挖一点内存出来,一旦能挖到足够的内存,就调用chunk_alloc再试一次并返回
for( int i=size; i< m_kMAX_BYTES, i+= m_kALIGN){
obj** my_free_list = m_free_list + FREELIST_INDEX(i);
obj* p = *my_free_list;
if(0!=p) { //该更大的空闲链表里面尚有可用区块,卸下一块给后备
*my_free_list = p->free_list_link;
m_start_free = (char*)p;
m_end_free = m_start_free + i;
return chunk_alloc (size,nobjs); // 此时的后备区一定能够供应至少一块需求区块的
}
}
// 程序能运行到这里,真的是山穷水尽了
m_end_free = NULL;
// 改用第一级分配器,因为一级分配器有弃而不舍得问系统要内存的精神
m_start_free = (char*)malloc_alloc::allocate(bytes_to_get);
}
// 已经成功的从系统中要到所需要的内存
m_heap_size += bytes_to_get; //纪录一共申请的内存数目
m_end_free = m_start_free + bytes_to_get; //把申请到的内存拨给后备区使用
return chunk_alloc (size,nobjs); //最后再试一次
}
终于完了!手写的都酸死了。还有3个遗留问题以后再讨论吧:
1. 关于 m_heap_size的运用
2. 在chunk_alloc的源代码中,试图从空闲链表挖内存时有这样一段注释,我还不太理解:
Try to make do with what we have, That can’t hurt. We do NOT try smaller requests, since thattends to result in disaster on multi-process machines.
3. 如何编写 线程安全的 STL内存分配器
We allocate memory in large chunks in order to avoid fragmenting the malloc heap too much. We assume that size is properly aligned.
还是上王道-:)
char* chunk_alloc (size_t size, int& nobjs)
{
size_t total_bytes = size* nobjs; // 这里是我们的良好愿望
size_t bytes_left = m_end_free – m_start_free; // 后备区里面剩下的内存
obj* result = NULL; //返回的内存区
if(bytes_left >= total_bytes){ //后备区里面的存货足以满足我们的良好愿望
result = m_start_free;
m_start_free += total_bytes; //修正后备区里剩下的内存量
return result;
}else if(bytes_left >= size){ //后备区足以满足一个(含)以上区块要求
nobjs = bytes_left / size; //改变需求区块数,这是实际能满足的数目
total_bytes = size* nobjs; //改变需求总量
m_start_free += total_bytes;
return result;
}else{ //可怜啊,别说20个了,就算1个也给不起了
//于是准备从系统拿了,要么不开口,开口就要两倍
size_t bytes_to_get = 2* total_bytes + ROUND_UP(m_heap_size >>4 ); // 稍候解释
//这个地方厉害了,先把后备区域中剩下的内存给他链接到相应的空闲链表里面去
if(bytes_left > 0){
obj** my_free_list = m_free_list + FREELIST_INDEX(bytes_left);
//程序能到这里,后备区里面的内存一定只有一个区块,插到链表头部
((obj*)m_start_free)->free_list_link = *my_free_list;
*my_free_list = (obj*)m_start_free;
}
//问系统要
m_start_free = (char*)malloc(bytes_to_get);
if(0==m_start_free){ // 系统没有内存了
//想办法从各个空闲链表里面挖一点内存出来,一旦能挖到足够的内存,就调用chunk_alloc再试一次并返回
for( int i=size; i< m_kMAX_BYTES, i+= m_kALIGN){
obj** my_free_list = m_free_list + FREELIST_INDEX(i);
obj* p = *my_free_list;
if(0!=p) { //该更大的空闲链表里面尚有可用区块,卸下一块给后备
*my_free_list = p->free_list_link;
m_start_free = (char*)p;
m_end_free = m_start_free + i;
return chunk_alloc (size,nobjs); // 此时的后备区一定能够供应至少一块需求区块的
}
}
// 程序能运行到这里,真的是山穷水尽了
m_end_free = NULL;
// 改用第一级分配器,因为一级分配器有弃而不舍得问系统要内存的精神
m_start_free = (char*)malloc_alloc::allocate(bytes_to_get);
}
// 已经成功的从系统中要到所需要的内存
m_heap_size += bytes_to_get; //纪录一共申请的内存数目
m_end_free = m_start_free + bytes_to_get; //把申请到的内存拨给后备区使用
return chunk_alloc (size,nobjs); //最后再试一次
}
终于完了!手写的都酸死了。还有3个遗留问题以后再讨论吧:
1. 关于 m_heap_size的运用
2. 在chunk_alloc的源代码中,试图从空闲链表挖内存时有这样一段注释,我还不太理解:
Try to make do with what we have, That can’t hurt. We do NOT try smaller requests, since thattends to result in disaster on multi-process machines.
3. 如何编写 线程安全的 STL内存分配器