您的位置:首页 > 其它

SGI STL的内存分配器

2012-04-13 15:18 253 查看
/article/11514599.html

我在这里枉自揣测一下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)如果后备区域也没有了,才问系统要!

现在上王道,(不是美女照片-:)),上原代码

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子节内存后,把多拿的内存部门插入到对应的空闲链表头部。

继续上王道:

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,也就是一开始后备区里面什么也没有。

假设程序已开始,用户申请一块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)字节链入相应的空闲链表。

先看看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内存分配器
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: