一、空间分配器 allocator
从使用上看,空间分配在任何语言的任何组件都不需要我们去过多关心,因为语言、组件的底层肯定都比较完整的做了这件事情。
从实现上看,学习 allocator 的原理在源码学习中是首当其冲。因为没有空间分配,则无从谈起对象创建。这里说是空间分配,而不是内存分配,是因为也可以在内存之外的地方(如硬盘)分配空间。
分配器主要作用就是分配空间,根据规范,其需要实现一些接口,完成一些关于空间分配的功能。标准接口规范见附录(一)。
本文会提到以下几个方面:
- SGI STL 分配器介绍
- construct 和 destroy
- destroy 接收指针和迭代器的方法
- alloc 分配器
- 一层分配器
- 二层分配器 和 free-list
- 分配
- 释放
- 补充
- 全局函数
- uninitialized_copy
- uninitialized_fill
- uninitialized_fill_n
二、SGI STL 的 alloc
SGI STL 的分配器与众不同,也与标准规范不同,其名称是 alloc 而非 allocator,而且不接受任何参数。具体来说,想在程序中明确使用 SGI 分配器,不能写std::allocator<int>
,而要写成std::alloc
。
即使它不符合标准规范,也不会对我们使用造成任何影响,因为通常我们都使用缺省的分配器,而不会自己指定。而 STL 的每一个容器都指定缺省分配器为 alloc。
当然了,SGI 也定义了符合部分标准、名为 allocator 的分配器,但出于其效率原因,STL 从未使用它,也不推荐程序员使用。它只是把 ::operator new 和 ::operator delete 做了一层薄薄的封装,没有做优化。详细代码见附录(二)。
下面详细聊聊 SGI STL 实现的 alloc 。
一般而言,C++的内存分配和释放操作如下:
class Foo {...};
Foo * pf = new Foo; // 分配内存,构造对象
delete pf; // 析构对象,释放内存
- new 内含两步操作:(1)调用 ::operator new 分配内存(2)调用 Foo::Foo() 构造对象内容。
- delete 内含两步操作:(1)调用 Foo::~Foo() 析构对象(2)调用 ::operator delete 释放内存。
为了精细分工,分配器将这两个步骤分开做。内存分配由 alloc::allocate() 负责,内存释放由 alloc::deallocate() 负责;对象构造由 ::construct() 负责,对象析构由 ::destroy() 负责。
// STL规定分配器 allocator 定义于 memory 中
#include <memory>// memory 中含有两个文件
#include <stl_alloc.h> // 负责内存空间的分配和释放,定义了 一级、二级分配器。
#include <stl_construct.h> // 负责对象内容的构造和析构,有 construct 和 destroy 方法。// memory 中还有一个文件
#include <stl_uninitialized.h> // 定义了一些全局函数,用来填充fill 或者 复制copy 大块内存的数据
// 其中有如下方法
// un_initialized_copy()
// un_initialized_fill()
// un_initialized_fill_n()
// 这些方法不属于分配器的范畴,但与对象初值设置有关,对大规模元素初值设置很有帮助。
// 在效率上,最差会调用 construct,最佳会调用C的 memmove 进行内存移动。
(一)construct 和 destroy
对于对象的 construct 和 destroy 可以概括如下图所示。其源码见附录(三)
- construct 接收 指针p 和 初值value,会将value设置到p所指的空间上。
- destroy 可以接收 指针、迭代器。
- 基本类型指针:不做处理
- 对象类型指针:调用析构函数
- 迭代器:会判断析构函数是否为 trivial destructor(无用的、没必要的、无意义的析构函数)
- 是:则不做处理
- 否:调用 迭代器中每个元素的析构函数。
这里有个问题是,如何判断是否为 trivial呢?
答案是:使用 __type_traits<T>::has_trivial_destructor() ,该函数会返回 __true_type 或 __false_type,前者代表是trivial,后者代表是有意义的。
该类的具体实现需要去研究下 traits,这里先不展开。
(二)STL alloc
<stl_alloc.h> 负责了对象构造前的空间分配和对象析构前的空间释放,有下面几个设计原则:
- 向 system heap 申请空间
- 考虑多线程状态(为了将问题简化,这里不讨论多线程状态)
- 考虑内存不足的应变措施
- 考虑过多小块内存造成的碎片问题
C++ 的内存分配和释放主要使用 ::operator new() 和 ::operator delete(),这两个相当于C的 malloc() 和 free(),SGI 正是以 malloc 和 free 完成的内存分配和释放。
SGI 设计了双层分配器,如下图所示:
- 第一级直接使用 malloc 和 free
- 第二级,当需求大于128 bytes 时,调用一级分配器;小于等于128 bytes 则调用二级分配器。
具体采用哪种分配器,需要看 __USE_MALLOC 是否被定义。定义了则用一级分配器,否则调用二级分配器。
SGI 为 alloc 提供了一个 simple_alloc 的接口封装,使得外层使用时无需考虑内部具体用的一级还是二级。SGI STL 的容器都使用这个 simple_alloc 接口,而非直接使用 alloc。代码见附录(四)。
一级分配器的原理比较简单,正常情况就是调用 malloc 和 free 做分配和释放。当内存不够时需要使用 oom_malloc,在该函数中,会循环调用一个 handler 来处理内存不足的情况。这个 handler 是需要自己指定的,如果没有指定,则抛出 std::bad_alloc 异常。这个 handler 一般称为 new-handler,在 《Effective C++》2e item7 中有特定的解决模式。
(三)STL 二级分配器
下面着重说说二级分配器。
二级分配器可以避免产生过多的小区块,可以解决内存碎片和过多的额外开销(系统需要多出来的空间管理内存,可以说是给系统“交税”)。
二级分配器以内存池(memory pool)管理小于128 bytes 的内存,称为次层分配(sub-allocation):先分配一大块内存,组成一个自由链表(free-list),每次要取一定量内存时,从 free-list 中取;在用完后,分配器就归还给 free-list。
分配器会维护 空间为 8、16、24、……、128 这16个 free-list,在分配小内存时,会向上取整(Round Up),寻找最近的 free-list。
free-list 节点结构是一个联合体,该节点在free-list中时,内容是一个指向 下一个节点的指针,在客户端使用时,是具体的数据。这样一物二用,不会造成维护链表指针的内存浪费。这个技巧在强类型语言(Strong Typed)中如 Java 行不通,但在弱类型语言(Weak Typed)中如 C++十分常见。
union obj{union obj * free_list_link; char client_data[1]; // client use
}
次层分配中从 free list 分出内存的步骤 allocate 如下图所示:
次层分配中释放内存,往 free list 中归还的步骤 deallocate 如下图所示:
当 free-list 的空间用尽后,会触发 refill 操作,重新给 free-list 补充 20个节点。refill 会调用 chunk_alloc,该函数中会做具体从内存池中取内存的操作。其过程如下所示。
简而言之就是,先找自己(32找32),再找亲友(64找32),实在不行就求助大家(96找32)。
三、内存基本处理工具
STL 定义了五个全局函数,除了前文提到的 construct 和 destroy,还有3个用来处理大块内存的复制和移动的 unitialized_copy、uninitialized_fill、uninitialized_fill_n 分别对应高层次的函数 copy、fill、fill_n。
unitialized_copy 函数让内存配置与对象构造行为分开。如果目标地址指向的空间都是未初始化区域,则会直接把源区域的对象产生复制品直接放到目标地址。STL 规范中要求该函数具有原子性,要么全部构造出来,要么全部不构造。
uninitialized_fill、uninitialized_fill_n 也和 unitialized_copy 类似。
这三个函数都会判断 对象是否为 POD(Plain Old Data,标量 or 传统 C 结构体),POD 会具有 trivial 函数,如果是 POD 则用最有效率的方法,如果非 POD 则用最安全的方法。过程大致如下所示。
附录
(一)标准接口规范
根据 STL 规范,allocator 必须要实现以下接口。
(二)SGI allocator 源码
下面是 SGI 实现的 allocator 全貌