目录
1 分配器内存实质
-
分配器是容器的幕后英雄,负责内存的使用,但是不建议用户直接使用它。因为没有需要用它,它是专门为容器服务的,了解分配器可以更好的掌握容器的操作效率,方便我们挑选合适的容器;
-
分配器操作内存的实质还是调用C Runtime Library的malloc和free(可以说c++的new以及任何其他上层操作内存的方式最终一层层调用都是回归到malloc和free这两个函数上),这两个函数再根据不同平台再去调用各自的System API。
系统申请的内存如下:
- 所以每次申请的内存后系统分配给我们的远远大于我们实际申请的空间,因为系统要给这块内存附加一定的标识(也可叫做cooking或者额外开销),以方便释放操作,因为附加的部分是固定的,也就说我们申请的内存越大,附加部分占用的比重就越小。
- 所以一个优秀的分配器,应当尽可能的让这些额外的空间占比更小,让速度更快。
2 分配器源码实现
上面我们提到了分配器的评判标准,现在我们来看一下编译器自带的标准库中的分配器是如何实现的。VC6,BC5,GC2.9的标准库分配器并没有做特殊设计。就是调用malloc和free。
缺点如下:
- 1.接口设计不方便。如果我们单独调用分配器,那么我们需要记住我们指向分配的那片内存空间的指针,以及分配的内存空间大小。不然我们无法使用deallocate来释放这份空间。虽然容器不会有影响。
- 2.如果我们需要多次分配空间,默认的分配器由于每次分配的空间都很小,导致我们需要进行很多次内存分配的操作,同时需要很多额外空间。那么这个没有特殊设计过的分配器在这种情况下的效率就会变得低下,影响程序运行效率
2.1 VC6标椎库allocator实现原理:
2.2 BC5标椎库allocator实现原理:
BC5的分配器与VC6没有本质区别。BC5的优点是他的分配器第二参数有一个默认值,让我们在调用分配器时方便了一些。
2.3 GCC2.9标椎库allocator实现原理:
虽然GC2.9和上面也基本一致,但是它有额外声明不要使用这个标准库的分配器,同时allocator这个标准库分配器没有被使用。它使用的分配器是自行修改的。
2.4 GCC2.9使用的是一个叫alloc的分配器:
【注意】:
- 如果我们每次放入容器的元素太小,比如放一个long(4个字节),那内存产生的额外的开销将会很大;如果放入100万个,那额外开销可能比你放的100万个long还要多,内存消耗将会很可怕的。
3 分配器的使用
虽然GC2.9和上面也基本一致,但是它有额外声明不要使用这个标准库的分配器,同时allocator这个标准库分配器没有被使用。它使用的分配器是自行修改的。
GCC2.9使用的是一个叫alloc的分配器:
- 由此可见,分配器的使用在释放时需要我们自己去记得当初申请了多少内存,显然是不方便的,但是我们在使用容器时不用关心,结论就是个人慎用。
4 分配器工作模式
用分配器而不直接用C RunTime Library提供的malloc函数,其本质上是减少内存的额外开销,也就是说分配器会预先开好一定的空间,尽量减少malloc的使用次数,它不是需要多少开多少,也就是我们常说的内存池设计。
5.GCC2.9的分配器的效率提高思路
5.1 内存空间简介
通过面向对象高级编程(上)的学习,我们可以将malloc分配出的内存区块分为这几个部分。
5.2 G2.9 分配器——alloc
从上面对内存空间的分析可以知道,malloc分配出的内存区块中需要有地方来存放这个内存区块的大小。然而对于同一个容器而言,它的内置类型应当是相同的,所以对于容器的分配器,我们可以对此作出优化。
alloc创建了16条单向链表用来存放数据。这些单向链表用来存放不同元素大小的数据。
当容器需要内存时,alloc先查看自己是否已经申请过了这个大小的内存,如果已经申请过了,那么就继续放在对应的单向链表尾部。否则再调用malloc向系统申请一块内存空间。具体可以查看这里【C++内存管理】G2.9 std::alloc 运行模式
它的优点就是,由于每个链表都只有一种大小的元素,那么对于这条链表上的每一个元素,我们就不必再单独使用内存空间来记录它的大小。从而节省了内存空间.
在这张图中可以看到很多看起来非常杂乱的连线,这个实际上是alloc的内存申请机制影响的,alloc在申请内存时会考虑之前剩余下来的内存余量(这里存在pool当中),如果有内存余量的话在下一次申请空间时,会将上一次分配剩下来的内存空间按照需要的大小进行切割并挂载到对应的节点上。如果上一次剩余的大小不足以划分,那么会将这个剩余的内存空间挂到与它相等的内存空间大小的节点上去,然后重新分配内存。具体可以参考这里
在G4.9中,分配器变成了new_allocator,旧的分配器alloc改名为_pool_alloc。
6.(补充)SGI STL的两级分配器
STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放如下:
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也可以直接作为第一级分配器。