STL中的空间配置器读书笔记
SGI-STL中定义了两套空间配置器,分别是std::allocator和std::alloc。
std::allocator只是在::operator new和::operator delete上做了一层简单的封装。所以一般也不会使用allocator,而alloc则是一套相比allocator在设计上更符合规范的组件。
这里有个点需要提及。
全局的::operator new/delete和C++中的关键字new/delete。
两者看起来是基本没有区别的,至少在名称上。实际上的功能则不相同。
我们在new一个对象的时候,在一个对象构造的出生过程中会发生以下两个操作。
class A{}
A* pa=new A;
关键字new会先调用::operator new进行内存的申请,然后调用类A的构造函数A::A()完成对象的内容初始化,通常使用者会在构造函数中进行一些成员必要的初始化工作,就是在这一步中起作用的,当然编译器也会替我们完成一些默认的工作,如:为int这种基本类型的成员初始化为默认值,如果我们没有为构造函数添加内容。
同理delete关键字
delete pa;
会先执行类A的析构函数,在调用::operator delete释放内存。在window系统中以及SGI-STL中的::operator new/delete是通过malloc/free来进行内存分配和释放的工作的。
下面则是对std::alloc的一些关键地方进行记录和理解。
alloc把内存分配->对象构造->对象析构->内存释放这4个过程分开为4个操作分别负责。
对应关系为:
内存分配 ——alloc::allocate()
对象构造 ——alloc::construct()
对象析构 ——alloc::deallocate()
内存释放 ——alloc::destroy()
SGI的内存分配策略中使用了两级配置器,当对象申请内存区块大小大于128bytes的时候,会直接调用一级配置器,一级配置器直接调用malloc直接进行分配。
一级配置器实现上基本是对malloc和free的一层封装,并且加上了一个handler机制来处理内存不足的情况,handler机制主要是在malloc申请内存失败的时候调用一个_S_oom_malloc()
函数来处理。
相对应的还有realloc操作也是类似的策略。
如果申请的是比较小的对象,大小不大于(<=)128bytes时,会使用二级配置器进行分配
为了处理小区块造成内存的碎片的问题,二级配置器使用了比较复杂的策略,简单的说由自由链表和内存池机制组成。
二级配置器的逻辑,当申请的区块大小大于128bytes时,直接由一级配置器负责分配内存。反之,会去自由链表中查找适合的分块。
自由链表是由下面这个联合体结构构成的一个大小为16的链表数组。
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
数组中的元素从下标0~15依次负责维护一条区块大小为8,16,24,32…128(以8为倍数)的内存链表。
即:
比如当我们申请一个大小为12byte的对象时,则配置器会向上调整至8的倍数,去维护16字节区块的链表(free_list[1])中寻找可用内存,并返回一个指向大小为16字节的空间的起始地址的void*
指针(这当中有4个字节在使用中是浪费掉了的)。
那么问题来了,初始的时候自由链表应该是没有区块的,那么区块又是从哪里来的呢?
这里就要说说内存池的机制了。
下图可见当链表的区块用完后,将会执行_S_refill()
函数将链表重新填充
而_S_resill()
函数会通过_S_chunk_alloc()
在内存池中抽取20块相应大小的内存,如果只能抽取到一个可用区块,则直接返回给用户。反之,将第一块返回给用户,剩余的链接到负责维护相应区块大小的自由链表上。
而在chunk_alloc()
中才是对内存池的操作。
假设程序一开始,第一次调用了chunk_alloc(32,20)
,则会malloc申请40个32bytes的区块,第一个返回给用户,剩下19个用来链接到free_list3上,剩下的20块区块留给内存池中,接下来又有内存申请的请求且最终调用了chunk_alloc(64,20)
这是free_list[7]没有挂载内存区块,则需要去向内存池请求内存,由于此时内存池只能够供应10个64bytes的区块,所以会全部返回,且第一个给用户,剩下9个链接到free_list[7]上(注意20个区块不是强制的,是尽可能的从内存池中拿20个区块出来)。如果此时又发生chunk_alloc(96,20)
的调用,此时free_list[11]和内存池都是空的,就会进行malloc内存申请,申请40+n个区块,n的大小计算如下图round_up()
所示
然后,1个返回给用户,19个链接到free_list[11]中,余下20+n的大小留给内存池。
如果最终,系统的堆内存也耗尽了,malloc申请失败,chunk_alloc就会到free_list中寻找可用且区块足够大的内存来使用,如果还是找不到的话就会交由一级配置器来配置内存,前面说到一级配置器也是使用malloc来申请内存的,但是里面有内存不足的处理机制(oom),会尽可能的释放一些内存以满足当前使用,如果不行的话就会抛出bad_alloc异常。
最后总结一下:SGI_STL的空间配置器设计,首先分为两级配置器,申请区块大于128bytes时调用一级配置器直接使用malloc分配和free释放。小于128bytes时,调用二级配置器,分配自由链表数组和内存池来维护小型区块的内存来供使用,大小为16的自由链表数组(类型为指向数组),每一个元素分别维护一个链表,每个链表依次负责维护8,16,24….128bytes大小的内存区块。当自由链表没有可用空间时,会向内存池请求内存,内存池没有可用空间时会进行malloc申请空间。
所有的这些的操作都是为了避免向操作系统申请过多小型区块,造成系统堆内存碎片化,也就是说自由链表和内存池的内存在使用完毕后是不会归还给系统的,而是返还给自由链表维护以备下次继续用。最终实现的效果就是程序整体上向系统申请的都是大块的内存,可以有效避免内存碎片化。但是因为是在用户态上进行内存管理,就容易出现程序运行时,单一进程对内存的使用上出现存在占用过高,有效利用率低的问题。