STL 空间配置器
一、常规对象配置
我们使用C/C++是对于动态内存对象可以有两种方法申请:1、调用 malloc() + free() 函数;2、调用 new + delete 运算符。
不得不提一下这两种方法的区别,虽然看起来都是向堆区(C++虽然叫自由存储区,但的确是通过堆区实现的)申请内存,但是new和delete在C++中支持C++的高级特性,如下表。比如new、delete支持重载,而且对于初始内存两者的处理完全不同。
特征 | new/delete | malloc/free |
---|---|---|
内存分配返回值 | 完整类型指针 | void* |
分配内存大小 | 编译器计算 | 显示指定字节数 |
已分配内存的扩充 | null | realloc |
构造函数和析构函数 | 自动调用 | null |
函数重载 | 允许 | 不允许 |
分配内存不足(下面会提到) | 客端需要指定处理函数(c++ new handler) | 用户无法插足 |
打个比方:malloc() 申请的内存是一块荒地,上面可能荒芜一片或者布满杂草(实际上确实是一个 void* 的返回值);而 new 产生的是一个经过耕耘,精心打理过得沃土(经过了构造)。区别就是 new 在申请内存后,还进行了构造函数的调用,同时 delete 时还会调用析构函数。那么我们引出两个概念:construct()和destroy() 。假设这两个函数是对空间的初始化和去初始化,我们用下列伪代码来模拟 new 和 delete 。
/* 这个new就是模拟 operator new 的行为
对象生命期: 内存申请 --> 对象构造 --> 对象析构 --> 内存释放
*/
//模拟new
void* operator new(size_t n)
{
void * ret = malloc(n); //申请内存
construct(ret); //对象构造
return ret;
}
//模拟delete
void operator delete(void * ptr)
{
destory(ptr); //对象析构
free(ptr); //释放内存
}
好了,到现在我们知道了构造对象的整个流程,截下来就让我们看一下STL空间配置的策略。
二、STL 的双层空间配置器
STL设计中考虑到的一个主要问题就是:
- 向 heap 申请内存;
- 内存不足时的应变策略;
- 内存申请产生的“内碎片”;
我们跟着这三个问题来看看STL是如何解决的。
第一个问题和第三个问题殊途同归,基本上都被STL的双层配置器解决了。第一级配置器直接使用 malloc() 和 free() ;第二级配置器则视情况采用不同的策略:当配置区块超过128 bytes 时,将调用第一级配置器;当配置器区块小于128 bytes 时为了降低额外负担,采取一个复杂的 memory pool 整理方式,而不是使用第一级配置器。下面代码概述第一级空间配置器和第二级空间配置器的职能
/*STL 第一级配置器:
1、allocate() 直接使用 malloc() , deallocate() 直接使用 free()
2、模拟c++的 set_new_handler()以处理内存不足的情况
*/
template<int inst>
class __malloc_alloc_template{...};
/*STL 第二级配置器:
1、memory pool中的16个自由链表,负责16中不同大小的小型区块的次配置能力;如若内存不足转调第一级配置器(这种情况一般是需要补充内存池容量了)
2、如果区块要求大于128 bytes,就转调第一级配置器
*/
template<bool threads, int inst> //第一个参数跟STL的多线程有关,但是有关STL多线程过于庞大以后赘述,第二个参数没用
class __default_alloc_template{...};
第一级不用多说了,纯纯的调用malloc、free,重点是第二级空间配置器。
三、第二级空间配置器 __default_alloc_template
第二级空间配置器多了一些机制,避免太多小额区块造成的内存碎片。不仅如此,申请小区块带来的额外负担也要大。因为 malloc 和 free 需要陷入内核,所以减少 malloc 调用次数非常重要。而简单的策略就是使用内存池,申请一大块内存,然后再将内存分配成小块供用户申请。
第二级配置器的做法是,区块大于128 bytes 移交给第一级配置器。当区块小于128 bytes 则由内存池管理:每次配置一大块内存,并维护对应的自由链表。下次若再有相同大小的内存需求,就直接从free-lists中拨出,如果客端需要归还小额区块,就由配置器回收到free-lists中。free-lists节点如下:
/*free-lists 中节点都是 8 的倍数 字节*/
union obj{
union obj * free_list_link;
char client_data[1];
}
一物二用,不会为了维护链表所必须的指针而造成内存的另一种浪费。
空间配置函数allocate() 是一个重要标准接口。此函数首先判断区块大小,根据之前所说的策略进行调整,如果 pool 中内存不足,调用refill(),源码如下:
static void * allocate(size_t n)
{
obj* volatile * my_free_list;
obj* result;
//大于128
if(n > (size_t) __MAX_BYTES){
return (malloc_alloc::allocate(n));
}
//寻找16个free中合适的
my_free_list = free_list + FREELIST_INDEX(n);
result = *my_free_list;
if( result == 0){//没有可用的 free-lists,调用refill
void* r = refill(ROUND_UP(n));
return r;
}
//调整free list
*my_free_list = result->free_list_link;
return (result);
}
四、总结
STL高效的内存利用率离不开双层结构的配置器:
- 大于 128 bytes,一级空间配置器
- 小于 128 bytes,二级空间配置器 + 高效的 memory pool 机制