前言
这一系列文章都是对STL的讲解,本文接着上篇第一级配置器,来讲解第二级配置器。第二级配置器是空间配置器的重点内容,也是相当麻烦的一部分内容。本文除了对《STL源码剖析》书中内容的讲解外,还自实现了 reallocate()函数,可以说相当不易,提前感谢大家的收看。
文章目录
一、一二级空间配置器的关系
- SGI STL 一级配置器
template<int inst>
class __malloc_alloc_template {};
其中,allocate()直接使用 malloc(), deallocate()直接使用 free()。
模拟C++的 set_new_handler()以处理内存不足问题。
- SGI STL 二级配置器
template<bo0l threads, int inst>
class __default_alloc_template {};
其中:
2.1 维护16个自由链表( free lists ) ,负责16种小型区块的次配置能力。
内存池(memory pool) 以malloc() 配置而得。如果内存不足,转调用第一
级配置器(那儿有处理程序)。
2.2 如果需求区块大于128 bytes,就转调用第一级配置器。
二、一二级配置器的接口和运用方式
三、二级配置器——__default_alloc_template
第二级配置器多了一些机制,避免太多小额区块道成内存的碎片。小额区块带来的其实不仅是内存碎片,配置时的额外负担也是大问题。
额外负担永远无法避免,毕竟系统要靠这多出来的空间来管理内存。
但是区块愈小,额外负担所占的比例就愈大,愈显得浪费。
1.什么是小额区块
小额区块怎么理解?
在C程序向堆区使用malloc()申请空间时,除了指定获得的字节个数,还需要向系统申请越界位置。
示例:
#include<malloc.h>
int main(void)
{
int* p = (int*)malloc(sizeof(int));
free(p);
p = NULL;
return 0;
}
通过内存和监视器可以看出,这个申请的空间还有上越界和下越界标记,那么如果这种小额空间申请的次数越多,标记信息也越多,显得越浪费。
2. 维护自由链表
SGI 第二级配置器的做法是,如果区块够大,超过128 bytes时,就移交第一级配置器处理。 当区块小于128 bytes时,则以内存池( memory pool)管理,此法又称为次层配置( sub-allocation):每次配置一大块内存,并维护对应之自由链表(free-list )。下次若再有相同大小的内存需求,就直接从free-lists中拨出。如果客端释还小额区块,就由配置器回收到free-lists中 —— 是的,别忘了,配置器除了负责配置,也负责回收。为了方便管理,SGl 第二级配置器会主动将任何小额区块的内存需求量上调至8的倍数(例如客端要求30 bytes,就自动调整为32 bytes) ,并维护16个 free-lists,各自管理大小分别为8,16, 24,32,40,48,56,64,72,80,88,96,104,112,120,128 bytes的小额区块。free-lists的节点结构如下:
代码
这里的 free_list_link也可以理解为链表节点的next域
enum { __ALIGN = 8 }; // 小型区块的上调边界
enum { __MAX_BYTES = 128 }; // 小型区块的上限
enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; // free_lists个数
private:
union obj
{
union obj* free_list_link; // next;
char client_data[1];
};
// 一个指针数组,自由链表
static obj* volatile free_list[__NFREELISTS];
图示
那么该自由链表可以看作这样:
辅助函数ROUND_UP() / FREELIST_INDEX()
// 将bytes上调至8的倍数
static size_t ROUND_UP(size_t bytes) // 1~8 / 9~16...
{
return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);
}
// 根据区块大小,决定使用第n个free_list
static size_t FREELIST_INDEX(size_t bytes)
{
return (bytes + __ALIGN - 1) / __ALIGN - 1;
}
3. 空间配置函数allocate()
此函数首先判断区块大小,大于128 bytes就调用第一级配置器,小于128 bytes就检查对应的free_list。如果 free_list 之内有可用的区块,就直接拿来用,如果没有可用区块,就将区块大小上调至8倍数边界,然后调用refill () ,准备为free list重新填充空间。refill()将于稍后介绍。
代码
public:
static void* allocate(size_t size)
{
// 如果大于128bytes,交给第一级配置器
if (size > (size_t)__MAX_BYTES)
{
return malloc_alloc::allocate(size);
}
obj* result = nullptr;
obj* volatile* my_free_list = nullptr;
my_free_list = free_list + FREELIST_INDEX(size);
result = *my_free_list;
if (nullptr == result)
{
void* r = refill(ROUND_UP(size));
return r;
}
*my_free_list = result->free_list_link;
return result;
}
分析:假设size的大小为20.
- 如果申请的空间大于128字节,就交给第一级配置器。
- 声明一个结点类型的返回值result和一个二级指针my_free_list供后面使用。
- 根据size的大小计算出应该从free_list的哪个节点上去取空间。(free_list表示自由链表的首地址,size为20,那么就应该在17~24范围内的结点,即第三个结点(free_list + 2) 上去取空间。)
- 那么二级指针my_free_list指向需要去取空间的那个结点。
- 通过一级指针result取得二级指针的第一个结点,即第一个24字节的内存块的地址。
- 如果有内存块,就将其取出来,并将二级指针my_free_list(解引用后)指向result的next域(result->free_list_link)。
- 如果该结点没有内存块了,就需要交给填充函数refill()来实现,稍后会说到。
4. 空间释放函数deallocate()
和allocate()相反,allocate()是链表的头删法,此函数相当于头插法。
该函数首先判断区块大小,大于128 bytes就调用第一级配置器,小于128 bytes就找出对应的free_list,将区块回收。这里就不多解释。
public:
static void deallocate(void* p, size_t n)
{
if (n > (size_t)__MAX_BYTES)
{
return malloc_alloc::deallocate(p, n);
}
obj* q = (obj*)p;
obj* volatile* my_free_list = nullptr;
// 寻找相应的free_list
my_free_list = free_list + FREELIST_INDEX(n);
// 头插法,回收区块
q->free_list_link = *my_free_list;
*my_free_list = q;
}
图示:
5. 内存池
一个从堆区申请的空间,heap_size表示该空间总大小;
start_free表示内存池起始位置,end_free表示内存池结束位置。
private:
static char* start_free;
static char* end_free;
static size_t heap_size; // total
// 配置一大块空间,可以容纳nobjs个size大小的区块
// 如果配置nobjs个区块有所不便,nobjs会做出相应改变
static char* chunk_alloc(size_t size, int& nobjs);
// 返回一个大小为size的对象,
// 并可能加入大小为size的其他区块到free_list中
static void* refill(size_t size);
简图:
chunk_alloc()
该函数的工作是,从内存池中取出空间供给free_list使用
private:
// 配置一大块空间,可以容纳nobjs个size大小的区块
// 如果配置nobjs个区块有所不便,nobjs会做出相应改变
static char* chunk_alloc(size_t size, int& nobjs)
{
char* result = nullptr;
// 需要空间的总大小
size_t total_bytes = size * nobjs;
// 内存池剩余空间
size_t bytes_left = end_free - start_free; // 1
// 如果内存池剩余空间满足需要空间
if (bytes_left >= total_bytes) // 2
{
result = start_free;
start_free = start_free + total_bytes;
return result;
}
else if (bytes_left >= size) // 3
{
// 如果剩余空间只够1个以上的块(但小于总需求)
nobjs = bytes_left / size;
result = start_free;
start_free = start_free + total_bytes;
return result;
}
else
{
// 如果连一块空间都不够 // 4
size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
// 将剩下的一点点空间再利用
if (bytes_left > 0) // 5
{
// 将剩余空间配置给合适的free_list
obj* volatile* my_free_list = free_list + FREELIST_INDEX(bytes_left);
((obj*)start_free)->free_list_link = *my_free_list;
*my_free_list = (obj*)start_free;
}
// 配置heap空间,补充内存池
start_free = (char*)malloc(bytes_to_get); // 6
if (nullptr == start_free) // 7
{
obj* volatile* my_free_list = nullptr;
obj* p = nullptr;
for (int i = size; i <= __MAX_BYTES; i += __ALIGN)
{
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if (nullptr != p)
{
*my_free_list = p->free_list_link;
start_free = (char*)p;
end_free = start_free + i;
return chunk_alloc(size, nobjs);
}
}
end_free = 0; // 8
start_free = (char*)malloc_alloc::allocate(bytes_to_get);
}
// 修正内存池的结束位置和总大小 // 9
end_free = start_free + bytes_to_get;
heap_size += bytes_to_get;
// 递归调用自己,修正nobjs
return chunk_alloc(size, nobjs);
}
}
解析
根据代码中的数字来看
该函数的参数:size表示区块的大小(8的倍数),nobjs表示需要多少块(注意这里是引用)。
- 声明返回值、所需要空间总大小、内存池剩余容量并计算出来。
- 如果容量足够,就将返回值指向start_free,并将start_free下移(移到减去total_bytes的位置),返回result。
- 如果容量不足所需要的,但是够一个以上的区块,就将nobjs修正,只返回当前所能提供的块数,然后和第二步一样。
- 如果连一个区块也无法提供:先声明一个将要向堆区申请的变量(bytes_to_get);
- 如果内存池容量还大于0,就先将剩余的零头配置到合适的free_list中去(利用FREELIST_INDEX)。
- 然后配置堆区空间,申请大小是bytes_to_get,如果申请失败,说明堆区没有资源了;
- 如果堆区也没有资源,那就查看free_list中的空闲空间,利用for循环,利用二级指针遍历free_list,如果有空间就释放出当前free_list的第一个区块,并让内存池的首尾指针指向该区块(start_free和end_free);然后递归调用自身函数,用来修正nobjs。
- 如果出现意外,就是到了山穷水尽的时候,就调用第一级配置器的allocate(),会导致抛出异常,这个宏在第一级配置器的开始有定义;
- 如果配置堆区资源成功,即不执行6、7步骤,那么更新heap_size和end_free,并递归调用自己,修正nobjs。
refill()填充函数
再说之前的allocate(),如果free_list中没有区块了,就利用refill()从内存池中取区块来配置到free_list,这里默认取得20个区块,若不足20块,会配置的结点数可能小于20(这里的区块数就是chunk_alloc中的nobjs)。
private:
static void* refill(size_t size)
{
int nobjs = 20;
char* chunk = chunk_alloc(size, nobjs); // 1
if (1 == nobjs) return chunk; // 2
obj* volatile* my_free_list = nullptr;
obj* result = (obj*)chunk;
obj* current_obj = nullptr;
obj* next_obj = nullptr;
int i = 0; // 3
my_free_list = free_list + FREELIST_INDEX(size); // 4
*my_free_list = next_obj = (obj*)(chunk + size); // 5
for (i = 1; ; ++i)
{
current_obj = next_obj; // 6
next_obj = (obj*)((char*)next_obj + size); // 7
if (i == nobjs - 1) //8
{
current_obj->free_list_link = nullptr;
break;
}
current_obj->free_list_link = next_obj; // 9
}
return result;
}
解析
参数size表示需要填充的区块的大小(也用来寻找在哪个free_list上填充)
- 默认申请二十个区块,并利用指针指向当前申请的空间。
- 如果chunk_alloc修正后只有一个块nobjs,就先将这个块返回。
- 声明一些变量,分别是:用来指向free_list、指向申请的空间、指向当前区块,指向下一个区块、循环的参数 i 。
- 利用FREELIST_INDEX找到待填充的free_list。
- 接下来的步骤都是将这块大空间分成当前小区快串连起来,所以这里先将chunk偏移size个大小(因为第一块结点需要给客户使用),并强转成结点指针类型,让next_obj和*my_free_list都指向这个地址。
- 每次进入这个无限循环,都先让current_obj指向next_obj的位置。
- 将next_obj强转成char类型后加上size个大小(意味着往下偏移一个块大小),再强转成obj*类型指针,然后让next_obj指向这个偏移后的位置,意味着next_obj指向了下一个区块。
- 如果此时 循环条件 i 和 nobjs - 1 相等了,说明把所需的所有的块分割连接完成了,那么就将current_obj的 next域(free_list_link)置为nullptr。
- 这一步是连接操作,让当前结点的next域(free_list_link)指向 下一个结点(next_obj)。
内存池实际操作结果:
6. 自实现函数reallocate()
实际上该函数也是比较好实现的,调用reallocate()时会面临这些情况:
-
old_sz > 128 && new_sz > 128;
即原空间和新空间都大于128,就调用第一级配置器。 -
old_sz 和 new_sz 都可以在同一个free_list上(比如 原:22字节,新:24字节;那么都在24字节这个free_list上取区块),那么可以不做处理,直接返回当前区块。
-
如果 old_sz > 128 && new_sz < 128;
old_sz < 128 && new_sz > 128;
old_sz < 128 && new_sz < 128;
接下来
围绕上面三点进行编写:
除去一二点,第三点可满足其他情况的发生具体流程是:
- 找出原大小和新大小哪个小;
- allocate()申请新空间;
- 将原空间的内容拷贝到新空间里,拷贝大小取决于原和新小的那个。
- deallocate()析构原空间。
- 返回新空间。
public:
static void* reallocate(void* p, size_t old_sz, size_t new_sz)
{
// 1
if (old_sz > (size_t)__MAX_BYTES && new_sz > (size_t)__MAX_BYTES)
{
return malloc_alloc::reallocate(p, old_sz, new_sz);
}
// 2
if (ROUND_UP(old_sz) == ROUND_UP(new_sz))
{
return p;
}
// 3
size_t sz = old_sz < new_sz ? old_sz : new_sz;
void* s = allocate(new_sz);
memmove(s, p, sz);
deallocate(p, old_sz);
return s;
}
第二级配置器源码
// 第二级配置器
enum { __ALIGN = 8 }; // 小型区块的上调边界
enum { __MAX_BYTES = 128 }; // 小型区块的上限
enum { __NFREELISTS = __MAX_BYTES / __ALIGN }; // free_lists个数
template<bool threads, int inst>
class __default_alloc_template
{
private:
// 链表
union obj
{
union obj* free_list_link; // next;
char client_data[1];
};
private:
// 一个指针数组,自由链表
static obj* volatile free_list[__NFREELISTS];
static size_t ROUND_UP(size_t bytes) // 1~8 / 9~16...
{
return (bytes + __ALIGN - 1) & ~(__ALIGN - 1);
}
static size_t FREELIST_INDEX(size_t bytes)
{
return (bytes + __ALIGN - 1) / __ALIGN - 1;
}
static char* start_free; // 内存池起始位置
static char* end_free; // 内存池结束位置
static size_t heap_size; // total
// 配置一大块空间,可以容纳nobjs个size大小的区块
// 如果配置nobjs个区块有所不便,nobjs会做出相应改变
static char* chunk_alloc(size_t size, int& nobjs)
{
char* result = nullptr;
// 需要空间的总大小
size_t total_bytes = size * nobjs;
// 内存池剩余空间
size_t bytes_left = end_free - start_free;
// 如果内存池剩余空间满足需要空间
if (bytes_left >= total_bytes)
{
result = start_free;
start_free = start_free + total_bytes;
return result;
}
else if (bytes_left >= size)
{
// 如果剩余空间只够1个以上的块(但小于总需求)
nobjs = bytes_left / size;
result = start_free;
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);
((obj*)start_free)->free_list_link = *my_free_list;
*my_free_list = (obj*)start_free;
}
// 配置heap空间,补充内存池
start_free = (char*)malloc(bytes_to_get);
if (nullptr == start_free)
{
obj* volatile* my_free_list = nullptr;
obj* p = nullptr;
for (int i = size; i <= __MAX_BYTES; i += __ALIGN)
{
my_free_list = free_list + FREELIST_INDEX(i);
p = *my_free_list;
if (nullptr != p)
{
*my_free_list = p->free_list_link;
start_free = (char*)p;
end_free = start_free + i;
return chunk_alloc(size, nobjs);
}
}
end_free = 0;
start_free = (char*)malloc_alloc::allocate(bytes_to_get);
}
// 修正内存池的结束位置和总大小
end_free = start_free + bytes_to_get;
heap_size += bytes_to_get;
// 递归调用自己,修正nobjs
return chunk_alloc(size, nobjs);
}
}
// 返回一个大小为size的对象,
// 并可能加入大小为size的其他区块到free_list中
static void* refill(size_t size)
{
int nobjs = 20;
char* chunk = chunk_alloc(size, nobjs);
if (1 == nobjs) return chunk;
obj* volatile* my_free_list = nullptr;
obj* result = (obj*)chunk;
obj* current_obj = nullptr;
obj* next_obj = nullptr;
int i = 0;
my_free_list = free_list + FREELIST_INDEX(size);
*my_free_list = next_obj = (obj*)(chunk + size);
for (i = 1; ; ++i)
{
current_obj = next_obj;
next_obj = (obj*)((char*)next_obj + size);
if (i == nobjs - 1)
{
current_obj->free_list_link = nullptr;
break;
}
current_obj->free_list_link = next_obj;
}
return result;
}
public:
static void* allocate(size_t size)
{
if (size > (size_t)__MAX_BYTES)
{
return malloc_alloc::allocate(size);
}
obj* result = nullptr;
obj* volatile* my_free_list = nullptr;
my_free_list = free_list + FREELIST_INDEX(size);
result = *my_free_list;
if (nullptr == result)
{
void* r = refill(ROUND_UP(size));
return r;
}
*my_free_list = result->free_list_link;
return result;
}
static void deallocate(void* p, size_t n)
{
if (n > (size_t)__MAX_BYTES)
{
return malloc_alloc::deallocate(p, n);
}
obj* q = (obj*)p;
obj* volatile* my_free_list = nullptr;
// 寻找相应的free_list
my_free_list = free_list + FREELIST_INDEX(n);
// 头插法,回收区块
q->free_list_link = *my_free_list;
*my_free_list = q;
}
static void* reallocate(void* p, size_t old_sz, size_t new_sz)
{
if (old_sz > (size_t)__MAX_BYTES && new_sz > (size_t)__MAX_BYTES)
{
return malloc_alloc::reallocate(p, old_sz, new_sz);
}
if (ROUND_UP(old_sz) == ROUND_UP(new_sz))
{
return p;
}
size_t sz = old_sz < new_sz ? old_sz : new_sz;
void* s = allocate(new_sz);
memmove(s, p, sz);
deallocate(p, old_sz);
return s;
}
};
// 对各个成员初始化
template<bool threads, int inst>
typename __default_alloc_template<threads, inst>::obj* volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] = {};
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = nullptr;
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = nullptr;
template<bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
//
//
// SGI STL
#ifdef __USE_MALLOC
typedef __malloc_alloc_template<0> malloc_alloc;
typedef malloc_alloc alloc;
#else
typedef __default_alloc_template<0, 0> alloc;
#endif
template<typename T, typename Alloc>
class simple_alloc
{
public:
// 申请
static T* allocate(size_t n) // n个T类型的
{
return Alloc::allocate(sizeof(T) * n);
}
static T* allocate()
{
return Alloc::allocate(sizeof(T));
}
// 删除函数
void deallocate(T* p, size_t n)
{
if (nullptr == p) return;
Alloc::deallocate(p, size(T) * n);
}
void deallocate(T* p)
{
if (nullptr == p) return;
Alloc::deallocate(p, sizeof(T));
}
};