STL源码剖析(三)第二级空间配置器
文章目录
上一篇文章中,我们讨论了空间配置器的作用,还有看了第一级空间配置器的源码,接下来我们来剖析这个第二级空间配置器的源码
一、为什么需要第二级空间配置器?
首先思考一下,为什么需要新的空间配置器?
因为在第一级空间配置器中,是直接采用malloc和free进行内存的申请和释放,这样做的优点是简单,但是它有很大的缺点。
一方面,mallc申请内存的时候,系统会附加一小块cookie来记录这块内存的大小,如果每次分配的内存都很小,那么这块cookie占总内存的比例就很大,使得内存的使用率不高
另一方,如果频繁地分配小块内存,那么势必会造成内存的碎片化,这是我们不愿意看到的
STL为了解决这个问题,实现了另一个空间配置器default_alloc_template
,它对内存做了精细的管理
理论上这个空间配置器非常的好,但令人疑惑的是,STL并没有沿用下来,反而使用的是上一篇文章讲的第一级空间配置器,但是这并不妨碍我们学习这个优秀的空间配置器
二、源码剖析
2.1 初步分析
这个空间配置器是default_alloc_template
,其定义如下
class __default_alloc_template {
private:
/* 管理内存块的节点 */
union obj {
union obj * free_list_link;
char client_data[1];
};
/* 管理内存的指针数组 */
static obj * __VOLATILE free_list[__NFREELISTS];
/* 维护内存池 */
static char *start_free;
static char *end_free;
static size_t heap_size;
public:
static void * allocate(size_t n)
{
...
}
static void deallocate(void *p, size_t n)
{
...
}
};
default_alloc_template
维护着一个内存池,内存池每次分配内存都会分配一大块内存,并维护free_list
,free_list
是一个指针数组,free_list
有16项,每一项都维护一个对应大小的内存块链表,大小分别为8、16、24、32、40、48、56、64、72、80、88、96、104、112、120、128,如下图所示
free_list
中的每个内存块节点使用obj管理,如下所示
union obj {
union obj * free_list_link; //指向下一个内存块
char client_data[1]; //表示数据
};
可以看到这里使用的是联合体而不是结构体,如果使用结构体,那么意味着每一个内存块都需要额外的一个指针来维护链表,而这个指针只有在free_list
中有用,当这个内存块被分配出去后,这个指针也就没有作用了。这里使用联合体的原因也在于此,其目的是为了节省内存,当节点应用free_list_link
则表示维护链表的指针,当节点引用client_data
,则表示这一整块内存
当调用这个空间配置器的allocate
分配内存的时候,如果要求的内存大于128,那么就会直接调用malloc进行分配,否则就会从free_list
中获取(要求的内存大小会8字节对齐)
当调用deallocate
释放内存的时候,如果大于128,那么就会直接调用free将其释放,否则会将这个内存块放入free_list
当中,下面将一一分析
2.2 分配内存(allocate)
分配函数的定义如下
class __default_alloc_template {
...
static void * allocate(size_t n)
{
obj * __VOLATILE * my_free_list;
obj * __RESTRICT result;
/* 如果大于128,则使用malloc */
if (n > (size_t) __MAX_BYTES) {
return(malloc_alloc::allocate(n));
}
/* 否则从free_list中获取内存,首先找到对应内存块大小的链表 */
my_free_list = free_list + FREELIST_INDEX(n); //找到指定内存大小的free_list
result = *my_free_list;
if (result == 0) { //如果该链表上没有内存,那么就重新填充
void *r = refill(ROUND_UP(n));
return r;
}
/* 删除被分配的内存块 */
*my_free_list = result -> free_list_link;
return (result);
};
};
上面的注释已经很清楚的,这里再理一下思路,首先,如果申请的内存大于__MAX_BYTES(128),那么就直接调用malloc进行分配。否则,从free_list找到管理指定内存块大小的链表,如果该链表上没有内存块,那么就重新填充,之后将分配得到的内存块从链表中删除,在返回此内存块
至于其中的refill
怎么填充的,稍后会专门分析
2.3 释放内存 (deallocate)
class __default_alloc_template {
static void deallocate(void *p, size_t n)
{
obj *q = (obj *)p;
obj * __VOLATILE * my_free_list;
/* 如果大于128,那么会直接使用free */
if (n > (size_t) __MAX_BYTES) {
malloc_alloc::deallocate(p, n);
return;
}
/* 从free_list中找到指定块大小的链表 */
my_free_list = free_list + FREELIST_INDEX(n);
/* 将该内存块插入到该链表的最前端 */
q -> free_list_link = *my_free_list;
*my_free_list = q;
}
};
上述程序中,首先如果内存块大于__MAX_BYTES(128),那么就会调用malloc_alloc::deallocate
进行释放,其会直接调用free。否则,首先从free_list
中找到指定内存块大小的链表,然后将要释放的内存块插入到该链表的最前端
2.4 free_list 填充(refill)
在分配内存时,我们没有分析refill
函数
refill
函数的作用是,从缓存块中获取内存,填充free_list
对应的链表
void* __default_alloc_template<threads, inst>::refill(size_t n)
{
int nobjs = 20;
char * chunk = chunk_alloc(n, nobjs); //要求从缓存块中索要20个节点大小的内存
/* 找到free_list中指定的链表 */
my_free_list = free_list + FREELIST_INDEX(n);
result = (obj *)chunk; //返回结果
/* 将剩余的内存连接到free_list指定的链表上 */
*my_free_list = next_obj = (obj *)(chunk + n);
for (i = 1; ; i++) {
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;
}
首先会从缓存块中分配20个节点大小的内存,但是并不一定有20个节点大小,分配完之后,会将一个节点大小的内存返回,剩余的内存添加到free_list
指定的链表中
2.5 缓存块(chunk_alloc)
下面再来看看缓存块是如何分配内存给的
其中start_free
是缓存块起始处,end_free
是缓存块结尾处
char*
__default_alloc_template<threads, inst>::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 { //如果缓存块大小不足一个节点的大小
/* 将剩余空间添加到指定的free_list中 */
if (bytes_left > 0) {
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;
}
/* 重新为缓存块分配内存 */
start_free = (char *)malloc(bytes_to_get);
heap_size += bytes_to_get;
end_free = start_free + bytes_to_get;
return(chunk_alloc(size, nobjs)); //递归调用
}
}
case1:如果缓存块剩余大小大于指定节点数的大小,那么就直接从缓存块返回
case2:如果缓存块剩余大小不满足指定节点数的大小,但是大于1个节点的大小,那么就返回最大节点数的内存
case3:缓存块剩余大小小于1个节点的大小,那么就将剩余内存添加到指定的free_list
链表中,然后重新分配大块内存,递归调用chunk_alloc