C++STL内存管理方法(g++版)
STL作为C++的经典作品,一直备受人们关注。本文主要介绍STL的内存管理策略。
早期的STL内存管理
第一次接触STL源码是看侯捷先生的《STL源码剖析》,此书通俗易懂,剖析透彻,是极佳的STL分析教程。不过由于是在2002年出版的,所以内容有些陈旧,不过仍然具有参考价值。
现代g++的STL是由SGI版的STL演化而来。
正如侯捷先生书中所讲,早期STL内存分配有两种方法:malloc/realloc/free
和内存池。默认的内存分配策略是内存池,下图中代码节选自stl_alloc.h
头文件:
由图中代码(代码摘自可知SGI3.3版本的STL源码),如果需要分配的内存容量大于128字节时便会转而调用malloc/realloc/free
版的内存分配方法。如果需要分配的内存大小小
于128字节便会调用内存池来满足需求,这种策略增加了代码的复杂性,但减少了内存碎片的问题。
malloc/realloc/free的方法
这种分配方法定义在stl_alloc.h
文件的__malloc_alloc_template
类模板中,它只是对malloc/realloc/free
的简单封装,并增加了一些措施用于处理内存不足时的情况(不断尝试分配,
释放,再分配,再释放)。
内存池
在SGI3.3中,内存池就是用一个指针数组存储指向一个个不同的固定大小的已事先分配好的链表头节点的指针。这句话可能有点绕口,分开说就是:内存池的主体是一个指针数组,数组中存储的每个
指针都指向一个链表,每个链表的节点大小都是固定的,这些链表的每个节点都是事先分配好的内存,这些内存供客户取用。用图来表示就是如下图:
图中的链表的每个节点是分开的,这是为了易于理解,真实情况是每个节点都是紧挨着的,因为每次分配一大块内存(用malloc
),然后按固定大小切分,并且用指针将它们连接起来。图中只画了7个
链表,真实情况是16个链表,各自管理着大小为8,16,24,32,40,48,56,64,72,80,88,96,104,112,128字节的区块。当需要的内存大小与这16个大小不匹配那么就向上舍入至最接近的大小(这会不
会造成更小的内存碎片?)。
有兴趣的可以看以下具体实现,在stl_alloc.h
文件中的__default_alloc_template
类中。从源码可以看出,内存池的实现比较复杂,特别是内存的分配和链表的构造。
现在的STL内存管理
现在的STL内存管理与早期的有很大不同,以g++5.4为例。从g++3.4开始,STL改变了原来的内存分配策略,默认的内存分配方式改为new/delete
。在Linux上查看当前系统的STL内存分配源代码的
顺序为:bits/allocator.h
-> bits/c++allocator.h
-> ext/new_allocator.h
。其中allocator.h
是对底层内存分配器的封装,供其他STL组件直接调用;bits/c++allocator.h
中只
有一行代码:
template<typename _Tp>
using __allocator_base = __gnu_cxx::new_allocator<_Tp>;
它定义了底层内存分配器为new_allocator
;ext/new_allocator.h
是底层分配器的定义文件,它定义了new_allocator
类,这个类只是对new/delete
的简单封装。
由于容器每次需要内存时调用
new
,释放内存时调用delete
,所以这种内存分配方法会比内存池要慢。但是其优势是在各种硬件和操作系统甚至大的集群中也能正确工作。另一种方法是使用
分配器中的缓存,这种额外的机制有多种实现形式:位图索引,一个以2的指数级成倍增加的桶;或者是一个简单的固定大小的缓冲池。分配器中的缓冲在一个程序的多个容器中共享。使用这些技术的
有bitmap_allocator
,pool_allocator
和mt_alloc
。由于不同的实现,不同的操作系统和不同的编译环境,扩展缓存分配器可能会非常棘手。特别是,内存池创建和析构的顺序可能很难确
定,当和插件一起使用或者在内存中装载和卸载共享对象时可能会产生问题。
以上内容节选自gcc关于内存分配的手册页中的说明,由此可见,gcc放弃使用继承自sgi的内存池而使用new/delete
是为了降低复杂度和增加可靠性。
gcc定义了几种内存分配方法:
-
new_allocator
对
new/delete
的简单封装,也是各种顺序容器和关联容器的默认内存分配器。 -
malloc_allocator
对
malloc/free
的简单封装,增加了对out-of-memory的处理。 -
array_allocator
允许使用通过构造
std::array
对象分配的现有全局或外部存储来分配已知和固定大小的内存。通过使用这个分配器,可以使用固定大小的容器(包括std::string
),而无需调用new
和delete
。此功能允许使用STL抽象,无需运行时复杂性或开销,即使在诸如程序启动的情况下。
-
debug_allocator
围绕任意分配器的包装器A.它将稍微增加大小的请求传递给A,并使用额外的内存来存储大小信息。 当指针传递给deallocate()时,检查存储的大小,并使用assert()来保证它们匹配。
-
throw_allocator
包括内存跟踪和标记能力以及在可配置的时间间隔抛出异常。
-
__pool_alloc
一个高性能,单池分配器。可重用的内存在这种类型的相同实例化之间共享。当它的内存用完时它调用通过运算符
new
获取新的内存。如果客户容器请求大于某个阈值大小的内存块,则绕过内存池,并且将分配/释放请求直接传递给new
。 -
__mt_alloc
一个高性能固定大小的分配器。
-
bitmap_allocator
一个高性能的分配器,使用位图来跟踪和标记使用和未使用的内存。
参考资料:
-
《STL源码剖析》侯捷 著 2002年