SGI STL中的空间配置器图文详解
1 为什么需要内存配置器?
1)在STL中,为了实现各种容器,需要分配内存,这个过程中如果直接在堆上开辟空间,可能会造成内存外碎片化。
1.内碎片化是指因为内存对齐、或者是按照其他的分配方式导致的已经分配的内存无法得到充分利用,比如需要5个字节,
实际分到了8个字节的情况。
2.外碎片化是指,内存中有足够的空间,但是这些空间都是不连续的零散空间,无法分配给申请内存空间的进程使用。
2)如果一直使用malloc,那么频繁的系统调用将会导致性能问题。
3)使用内存配置器可以提高代码的复用率,具有更好的拓展性。
2 SGI STL中空间配置器的基本原理
SGI STL allocator设置了两级空间配置器。分别是:一级空间配置器、二级空间配置器。当申请空间大于128Byte时调用一级空间配置器;小于等于128Byte时,调用二级空间配置器。
2.1一级空间配置器
2.2 二级空间配置器
3 重要代码详解(注释详尽)
3.1 二级空间配置器的allocate
二级配置器利用16条链表来管理小空间,每个链表由多小区块组成。一共有16条free-list链表,从第一个开始每一个链表管理的区块分别是8,16,24,32,40,48,56,64,72,80,88,96,104,112,120,128字节的小区块。
如果用户申请的字节数不是8的倍数,_round_up函数会将请求的空间上调到最近的8的倍数处。
//需求上调为8的倍数
static size_t _round_up(size_t __bytes){//这里技巧性比较强
return (((__bytes)+(size_t)_ALIGN - 1) & ~((size_t)_ALIGN - 1));
}
这些小区块由_obj节点组成,从第一个字段看,obj可以被看成是一个指向相同类型的另一个obj的指针,从第二个字段看它又可以被看成是一个指向实际区块的指针。这里利用了union的特点,把额外的指针空间都节省了下来。
//free-lists的节点构造
union _obj {
union _obj* _free_list_link;
char _client_data[1];
};
static void* allocate(size_t _n)
{
void* _ret = 0;
_obj** _my_free_list;
int _suit_size = _round_up(_n);
printf("需求适合的尺寸为:%d\n", _suit_size);
//如果大于小区块最大值直接采用一级空间配置器配置
if (_n > (size_t)_MAX_BYTES) {
_ret = MallocAlloc::allocate(_n);
}
else {
size_t _index = _freelist_index(_n);
_obj** _my_free_list = _free_list + _index;//找到16个中适合的小区块链表
_obj* _result = *_my_free_list;//把小区块链表的第一个区块地址给_result
if (_result == 0)//如果没找到合适的小区块,重新填充free-list
_ret = _refill(_round_up(_n));
else {
//调整free-list,让*_my_free_list指向目前链表的下一个区块
*_my_free_list = _result->_free_list_link;
_ret = _result;
}
}
return _ret;
}
3.2 二级空间配置器的free-list填充函数_refill
//_refill重新填充free-list
void* DefaultAlloc::_refill(size_t _n)
{
printf("当前refill次数为:%d\n", ++_test_t);
int _nobjs = 20;
//调用chunk_alloc(),尝试取得nobjs个区块作为free-list的新节点
//注意nobjs是引用传递,因为可能要修改nobjs的值
char* _chunk = _chunk_alloc(_n, _nobjs);
_obj** _my_free_list;
_obj* _result;
_obj* _current_obj, * _next_obj;
int _i;
//如果只获得一个区块,这个区块就分配给调用者使用,free-list没有新的节点
if (1 == _nobjs) return(_chunk);
//否则就调整free-list,纳入新节点
_my_free_list = _free_list + _freelist_index(_n);
//以下在 chunk空间内建立free-list
_result = (_obj*) _chunk;//这一块返回给客端(从char*转换为obj*)
//在chunk中建立free-list
_next_obj = (_obj*)(_chunk + _n);//这里表示让_next_obj指向_chunk后_n字节的位置,也就是下个区块开始的地方
*_my_free_list = _next_obj;//调整当前free-list
//把free-list的节点串起来
for (_i = 1; ; _i++)
{
_current_obj = _next_obj;
_next_obj = (_obj*)((char*) _next_obj + _n);//让_next_obj指向下一个区块开始的地方
if (_nobjs - 1 == _i) //最后一个区块,令_free_list_link(next)指针为空
{
_current_obj->_free_list_link = 0;
break;
}
else {
_current_obj->_free_list_link = _next_obj;
}
}
return(_result);//返回第一个区块地址
}
3.3 二级空间配置器的内存池填充函数_chunk_alloc
//假设_size已经适当上调至8的倍数,_size就是区块字节大小
char* DefaultAlloc::_chunk_alloc(size_t _size, int& _nobjs)//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;//把内存池开始的指针给_result
_start_free += _total_bytes;//内存池开始位置向后移动一段距离
printf("_chunk_alloc分配的空间为:%d \n" ,_total_bytes);
printf("_chunk_alloc分配的空间的地址为:%p \n", _result);
return(_result);
}
else if (_bytes_left >= _size) {//如果剩下的空间大于单个区块,小于总需求量
_nobjs = (int)(_bytes_left / _size);//算出能给的区块
_total_bytes = _size * _nobjs;//调整需求量为目前能给出的空间
_result = _start_free;
_start_free += _total_bytes;
return(_result);
}
//内存池连单个区块空间都不能提供
// 看看剩余空间能否分配给更小的区块用
else {
//扩容现有内存池大小的16分之一+原需求量的二倍
size_t _bytes_to_get =
2 * _total_bytes + _round_up(_heap_size >> 4);
if (_bytes_left > 0) {
//寻找适当的free-list
_obj** _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) {
//堆空间不足,malloc()失败
size_t _i;
_obj** _my_free_list;
_obj* _p;
//遍历大于size的free-list,搜索合适的区块
for (_i = _size; _i <= (size_t)_MAX_BYTES; _i += (size_t)_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;
//递归调用自己,修正_nobjs
return(_chunk_alloc(_size, _nobjs));
}
}
_end_free = 0; //出现意外,所有free-list都没有小空间了
//尝试使用一级配置器
_start_free = (char*)MallocAlloc::allocate(_bytes_to_get);
}
_heap_size += _bytes_to_get;
printf("现在内存池的大小:%d :\n", _heap_size);
_end_free = _start_free + _bytes_to_get;
//递归调用自己,为了修正nobjs
return(_chunk_alloc(_size, _nobjs));
}
}
3.4 填充的简单原理
4 总结
4.1 优点
1)可以很好地管理小区块内存,减少外碎片化。
2)代码模块化,提高复用率
4.2 缺点
1)造成内碎片化,即使你只需求1个字节,但是配置器也会给你分配8字节的小区块
2)无法把空间还给系统,只能在freelists自由链表和内存池之间交换,自由链表中的空间都是连续的。却没有释放。