分配器(Allocator)
分配器给容器用的,是一个幕后英雄的角色。分配器的效率非常重要。因为容器必然会使用到分配器来负责内存的分配,它的性能至关重要。
在C++中,内存分配和操作通过new和delete完成。
new中包含两个操作,第一步是使用operator new分配内存,第二步是调用构造函数;
delete中包含两个操作,第一步是调用析构函数,第二步是使用operator delete释放内存。
operator new() 和 malloc()
vc98
C++的内存分配动作最终都会回到malloc,malloc再根据不同的操作系统类型(Windows,Linux,Unix等)底层的系统API来获取内存
同时我们可以看到,malloc分配之后的内存块中不是只有数据,而是还包含了其它很多数据。这样容易联想到如果分配次数越多,那么内存中数据越零散,这些额外的数据开销就越大。
所以一个优秀的分配器,应当尽可能的让这些额外的空间占比更小,让速度更快。
VC6,BC5,GC2.9所带的标准库分配器源码分析
VC6
BC5
BC5的分配器与VC6没有本质区别。BC5的优点是他的分配器第二参数有一个默认值,让我们在调用分配器时方便了一些。
GC2.9
GC2.9自带的allocator也差不多
虽然GC2.9和上面也基本一致,但是它有额外声明不要使用这个标准库的分配器,同时这个标准库分配器没有被使用。它使用的分配器是自行修改的
GC2.9的分配器的效率提高思路
GC2.9使用的是一个叫alloc的分配器
1.内存空间简介
2.G2.9 分配器——alloc
malloc分配出的内存区块中需要有地方来存放这个内存区块的大小。然而对于同一个容器而言,它的内置类型应当是相同的,所以对于容器的分配器,我们可以对此作出优化。
alloc创建了16条单向链表用来存放数据。这些单向链表用来存放不同元素大小的数据。
当容器需要内存时,alloc先查看自己是否已经申请过了这个大小的内存,如果已经申请过了,那么就继续放在对应的单向链表尾部。否则再调用malloc向系统申请一块内存空间。具体可以查看这里【C++内存管理】G2.9 std::alloc 运行模式
它的优点就是,由于每个链表都只有一种大小的元素,那么对于这条链表上的每一个元素,我们就不必再单独使用内存空间来记录它的大小。从而节省了内存空间
#0所对应的节点连接的链表负责分配大小为8byte的子空间
#3所对应的节点连接的链表负责分配大小为32byte的子空间
以此以8byte的间隔向后类推
#15所对应的节点连接的链表负责分配大小为128byte的子空间
在这张图中可以看到很多看起来非常杂乱的连线,这个实际上是alloc的内存申请机制影响的,alloc在申请内存时会考虑之前剩余下来的内存余量(这里存在pool当中),如果有内存余量的话在下一次申请空间时,会将上一次分配剩下来的内存空间按照需要的大小进行切割并挂载到对应的节点上。如果上一次剩余的大小不足以划分,那么会将这个剩余的内存空间挂到与它相等的内存空间大小的节点上去,然后重新分配内存。
二级分配器
当需要一次性分配的内存超过了128byte,std::alloc()本身就不会为其服务,而会将这个需求转给其他函数去处理,这里我们会在后边剖析源码的时候去介绍
可是用户申请分配的空间可能并不会正好是8的倍数(通常都不是),这时,就会把他提升至最近的长度,并为其分配对应大小的内存块。
new运算分两个阶段:
(1)调用::operator new配置内存;
(2)调用对象构造函数构造对象内容
delete运算分两个阶段:
(1)调用对象析构函数;
(2)调用::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作区分开来:
内存配置有alloc::allocate()负责,内存释放由alloc::deallocate()负责;
对象构造由::construct()负责,对象析构由::destroy()负责。
同时为了提升内存管理的效率,减少申请小内存造成的内存碎片问题,SGI STL采用了两级配置器
当分配的空间大小超过128B时,会使用第一级空间配置器;当分配的空间大小小于128B时,将使用第二级空间配置器。
第一级空间配置器直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级空间配置器采用了内存池技术,通过空闲链表来管理内存。
当然,alloc也可以直接作为第一级分配器。