SGI STL 内存池源码剖析
大纲
前言
本文将剖析 SGI STL 二级空间配置器中的内存池源码,并介绍内存池的底层设计和工作原理,最后移植 SGI STL 内存池的核心源码。
C++ 常见的池
在 C++ 中有五大池,包括内存池、连接池、协程池、线程池、进程池。
源码下载
书籍推荐
STL
概念介绍
C++ 中的 STL(Standard Template Library)是标准模板库,提供了一组通用的数据结构和算法,如向量(vector
)、链表(list
)、集合(set
)、映射(map
)、排序、查找等;基于模板实现,支持泛型编程,代码复用性高、效率也好。
多种实现
除了 C++ 自身提供的 STL 之外,常见的第三方 STL(标准模板库)实现有以下几种:
RW STL(Rogue Wave STL)
- 开发者:
- Rogue Wave Software 公司(后被 Perforce 收购)
- 性质:
- 商业版本的 C++ 标准模板库实现
- 代表产品:
- 作为 SourcePro C++ 产品的一部分提供(包含 STL、线程库、网络库、数据库访问库等)
- 产品特点:
- 遵循 ISO C++ 标准
- 跨平台,适配多种操作系统和编译器
- 提供高级调试、异常安全、多线程支持
- 可配置性强,适用于嵌入式和大型系统
- 应用场景:
- 金融系统、通信系统、嵌入式开发、航空航天、政府项目等对可靠性要求高的场景
- 开发者:
PJ STL(Dinkumware STL)
- 开发者:
- P. J. Plauger(C++ 标准委员会成员),其自己公司 Dinkumware 提供支持
- 性质:
- 商业版本的 C++ 标准模板库实现
- 代表使用者:
- Microsoft Visual C++(尤其是 VC++ 6 及后续版本)默认使用 PJ STL
- 特点:
- 遵循 ISO C++ 标准
- 接口稳定、实现清晰,强调工程实用性
- 提供 STL、C 标准库(如
<cmath>
,<cstdlib>
)、TR1 和部分 C++ 11 / 14 特性
- 优势:
- 商业支持
- 与微软编译器深度集成
- 重视可移植性和性能
- 局限:
- 并非开源,文档和源代码可见性有限
- 开发者:
HP STL(Hewlett-Packard STL)
- 开发者:
- Hewlett-Packard 公司(惠普实验室),由 Alexander Stepanov 及其团队在 HP(惠普)初步实现
- 性质:
- STL 的原始实现,是 SGI STL 的前身
- 历史意义:
- 1994 年 Stepanov 在 HP Labs(惠普实验室)首次实现 STL,随后向 C++ 标准委员会提交
- 后由 SGI 改进并推广形成 SGI STL,逐渐成为业界学习模板编程的典范
- 特点:
- 结构清晰,贴近标准草案
- 强调通用算法和迭代器的分离设计
- 奠定了 STL 的基础架构,如容器、迭代器、算法的三大支柱
- 应用与影响:
- 虽然未被广泛部署于实际商业编译器中,但在学术界和标准委员会中具有开创性意义
- 是理解 STL 起源与设计哲学的关键资料
- 现状:
- 已不再维护,但源码与设计理念在 SGI STL 中得以继承和发扬
- 开发者:
SGI STL(Silicon Graphics STL)
- 开发者:
- Alexander Stepanov(STL 之父)、Meng Lee 等人在 SGI(Silicon Graphics Inc.)公司开发
- 性质:
- 最早公开发布的 STL 实现之一,并作为学习范例广泛传播
- 特点:
- 完全开源,源码可读性和学习性极强
- 强调泛型编程、模板技巧和性能优化
- 提供了
allocator
、traits
、function object
、iterator
等先进设计
- 贡献:
- 为后来的标准库实现(如 GCC 的 libstdc++)提供了基础
- 推动了 STL 的工业化、模块化和标准化
- 现状:
- 虽然已经不再更新,但作为教学和研究材料仍具重要历史价值
- 开发者:
第三方 STL 实现的总结对比表
名称 | 开发者 | 是否开源 | 典型应用 | 特点描述 |
---|---|---|---|---|
RW STL | Rogue Wave Software | ❌ 商业版 | 金融、嵌入式、政府、工业系统 | 工程化程度高,稳定性强,附带调试与多线程支持,属于 SourcePro C++ 一部分 |
PJ STL | P. J. Plauger / Dinkumware | ❌ 商业版 | Visual C++、商业工具链 | 工程稳健,兼容性好,被微软 Visual Studio 多版本采用 |
HP STL | Alexander Stepanov / HP | ✅ 开源 | 教育、历史研究、标准演化 | STL 的最早实现版本,设计哲学完整,影响了 SGI STL 和后续标准化进程 |
SGI STL | Alexander Stepanov / SGI | ✅ 开源 | 教育、研究、GCC libstdc++ 原型 | 泛型编程示范,模板设计前沿,影响广泛 |
SGI STL
概念介绍
SGI STL 是什么
SGI STL 是由 Alexander Stepanov 和 Meng Lee 等人在 SGI(Silicon Graphics Inc.)公司开发的 C++ 标准模板库实现,最早出现在 1990 年代。这个库实现了 STL 的核心思想:泛型编程与容器 - 算法 - 迭代器模型。SGI STL 包含了一级空间配置器和二级空间配置器,其中一级空间配置器 allocator
采用 malloc()
和 free()
来管理内存,和 C++ 标准模板库(STL)中提供的 allocator
是一样的,但其二级空间配置器 allocator
采用了基于自由链表(Free List)原理的内存池机制来实现内存管理。具体来说,当申请的内存块超过 128
字节时,视之为 “足够大”,会调用一级空间配置器;当申请的内存块小于 128
字节时,视之为 “过小”,会调用二级空间配置器,采用复杂的内存池管理方式,这样可以避免小内存频繁申请和释放导致的内存碎片问题,同时提高效率。
STL 空间配置器的作用
- 在 C++ STL 中,空间配置器(
allocator
)的作用主要有两种: - (1) 分离了对象的内存开辟(
allocate()
)和对象的构造(construct()
)。 - (2) 分离了对象的析构(
destroy()
)和对象的内存释放(deallocate()
)。
SGI STL 的主要特性
- 提供了 STL 的主要组件:
vector
、list
、map
、set
、algorithm
、iterator
等。 - 包含一些非标准扩展,例如:
slist
(单向链表)hash_map
/hash_set
(后来的unordered_map
/unordered_set
的前身)
- 代码高度模板化、效率高、注释详尽。
- 源码开放,很多 C++ 开发者都用它来学习 STL 的底层实现。
SGI STL 的历史作用
SGI STL 是 现代 C++ STL 的原型,其思想和实现对 C++ 98 标准 STL 的设计有极大影响。许多早期编译器,如 GCC、MSVC、Intel C++,都在某个时期采用过 SGI STL 或其变种(比如 STLport)。
今天还需要使用 SGI STL 吗
- 通常不需要使用 SGI STL,因为现代 C++ 编译器都自带符合标准的 STL 实现,比如 GCC:libstdc++、Clang:libc++、MSVC:Dinkumware。
- 如果是为了学习 STL 的内部实现机制(底层原理),SGI STL 是一个非常好的教学资源;但在生产环境中,应该使用现代 C++ STL(标准模板库)。
SGI STL 与 C++ STL 的区别
项目 | SGI STL | C++ STL(标准模板库) |
---|---|---|
作者 / 出处 | SGI 公司(如 STLport) | C++ 标准委员会 |
标准性 | 非正式实现,非标准的一些扩展 | 从 C++ 98 开始,成为标准 C++ 的一部分 |
扩展内容 | hash_map 、slist 、rope 等非标准组件 | 标准化容器如 vector 、deque 、map 、set 等 |
可移植性 | 与平台、编译器耦合程度较高 | 作为标准模板库,各主流编译器都支持 |
现代兼容性 | 不支持 C++ 11 及以后的特性 | 现代 STL 持续演进(如 move semantics , unordered_map , ranges 等) |
教育价值 | 非常适合学习 STL 实现机制 | 通常以黑箱方式使用 |
自由链表
这里将介绍 SGI STL 二级空间配置器中自由链表的底层实现。
自由链表是什么
自由链表是一种用于管理空闲内存块的链表结构。每个链表节点指向一个未被使用的内存块,便于快速分配和回收。在 SGI STL 的二级空间配置器中,自由链表有以下特点:
- 每种固定大小的内存块(如 8 字节、16 字节、24 字节 …)都有一个对应的自由链表。
- 小对象内存块的复用:当用户释放内存时,并不会直接
free()
,而是将内存放回对应的自由链表中,供后续复用。 - 分配内存时,优先从自由链表中取;只有自由链表为空时,才会从系统堆(Heap)分配一大块内存进行切分。
特性 | 描述 |
---|---|
数量 | 16 个自由链表(按 8 ~ 128 字节分为 16 类) |
数据结构 | union obj 实现链表节点 |
分配策略 | 优先从自由链表取,否则从堆中批量 refill() |
回收策略 | 小块内存回收到对应的自由链表,大块内存直接释放 |
性能优势 | 避免频繁 malloc / free ,显著减少内存碎片和内存开辟 / 释放的开销 |
自由链表的结构
内存块粒度与分类
- 内存块的粒度信息
1 |
|
- 一共维护 16 个自由链表,每个自由链表负责管理一种字节大小的内存块:
自由链表的下标 | 内存块的大小 |
---|---|
0 | 8 字节 |
1 | 16 字节 |
2 | 24 字节 |
… | … |
15 | 128 字节 |
自由链表节点结构
- 每个空闲内存块都可以看作是一个
_Obj
,其中_M_free_list_link
用来链接空闲内存块(最终形成自由链表)
1 | union _Obj { |
自由链表的存储结构
- 这个数组保存了每种块大小的链表头指针
1 | static _Obj* __STL_VOLATILE _S_free_list [_NFREELISTS]; // 16 个自由链表 |
内存分配策略
- 当调用
allocate(n)
:- 如果
n > 128
,则交由一级空间配置器处理(直接调用malloc()
分配内存)。 - 否则:
- (1) 将
n
向上对齐到 8 的倍数(实现内存对齐)。 - (2) 找到对应的自由链表索引。
- (3) 如果该链表非空,从中取一个满足大小的空闲内存块返回。
- (4) 如果链表为空,调用
refill()
从堆上批量分配一大块内存(默认分配 2 x 20 个块),挂到自由链表上。
- (1) 将
- 如果
内存回收策略
- 当调用
deallocate(p, n)
:- 如果
n > 128
,则交由一级空间配置器处理(直接调用free()
释放内存)。 - 否则:
- (1) 将
n
向上对齐到 8 的倍数(实现内存对齐)。 - (2) 找到对应的自由链表索引。
- (3) 把块头强制转换为
obj*
,插入到自由链表的头部(头插法)。
- (1) 将
- 如果
源码剖析
在剖析 SGI STL 的内存池源码时,建议以 vector
容器的源码作为切入点,然后一步步分析一级和二级空间配置器的底层实现,如下图所示:
空间配置器的相关定义
vector
容器的定义
1 | template <class _Tp, class _Alloc = __STL_DEFAULT_ALLOCATOR (_Tp) > |
vector
容器的默认空间配置器是__STL_DEFAULT_ALLOCATOR ( _Tp)
,它是一个宏定义,如下:
1 |
- 从上面可以看到
__STL_DEFAULT_ALLOCATOR
通过宏控制有两种空间配置器实现,一种是allocator<T>
,另一种是alloc
,这两种分别就是 SGI STL 的一级空间配置器和二级空间配置器的实现
1 | template <int __inst> |
1 | template <bool threads, int inst> |
- SGI STL 的一级和二级空间配置器对比
类型 | 实现类 | 内存管理机制 | 用途 | 源码位置 |
---|---|---|---|---|
一级空间配置器 | __malloc_alloc_template | malloc() / free() | 分配大块内存 | 定义在 stl_alloc.h 头文件,如图所示 |
二级空间配置器 | __default_alloc_template | 内存池机制 | 分配小块内存,其核心机制就是使用一组自由链表(Free List)来管理内存块,提高小对象(小于等于 128 字节)的内存分配效率,底层也是使用 malloc() / free() 分配和释放内存 | 定义在 stl_alloc.h 头文件,如图所示 |
SGI STL 默认的空间配置器
从上面的 vector
容器源码可以发现,SGI STL 的每一个容器都已经指定其缺省的空间配置器为 alloc
,即默认使用的是二级空间配置器 __default_alloc_template
。
重要类型和变量的定义
在 SGI STL 的二级空间配置器(__default_alloc_template
)中,有以下重要类型和变量定义,其底层的自由链表节点结构如下图所示:
- 内存池的粒度信息
1 |
|
- 每一个内存 chunk 块的头信息
1 | union _Obj { |
- 组织所有自由链表的数组,数组的每一个元素的类型都是
_Obj*
,全部初始化为0
1 | static _Obj* __STL_VOLATILE _S_free_list [_NFREELISTS]; |
- 记录内存 chunk 块的分配情况
1 | // Chunk allocation state. |
重要的辅助接口函数
- 将
__bytes
上调至最邻近的 8 的倍数(实现内存对齐)
1 | static size_t _S_round_up (size_t __bytes) { |
- 返回
__bytes
大小的 chunk 块位于自由链表数组中的索引
1 | static size_t _S_freelist_index (size_t __bytes) { |
内存池管理核心函数
1 | // 分配内存的入口函数 |
内存池管理总结说明
SGI STL 对内存分配和内存释放的设计哲学
- 向 System Heap 要内存空间。
- 考虑多线程(Multi Threads)状态
- 考虑内存不足时的应变措施
- 考虑过多 “小型区块内存” 可能造成的内存碎片(Fragment)问题
SGI STL 二级空间配置器中内存池的优点
内存分配具有预留机制
- 对于每种特定字节大小的 chunk(内存块)分配,内存池并不会仅分配刚好所需的内存,而是会一次性申请较大一块内存,并将其划分为多个 chunk(比如 40 个)。其中一部分(20 个 chunk)立即用于分配,另一部分(20 个 chunk)作为备用,供后续相同字节大小的分配请求使用。更重要的是,如果其他字节大小的 chunk 分配失败,这部分备用内存也有可能被重新划分后用于其他字节大小的 chunk 分配请求,从而提升内存使用的灵活性与效率。
最大化利用碎片内存
- 在将一大块备用内存划分为若干 chunk 后,可能会剩下一小部分无法完整划分的内存碎片。SGI STL 会在后续分配中尽可能再次利用这些内存碎片,避免资源浪费,确保备用内存池被 “用得干干净净”。
具有健壮的回退机制
- 当某一特定字节大小的内存块分配失败时,二级空间配置器会遍历所有字节大小的自由链表,查找是否有可用的空闲 chunk 块可暂时 “借用”。如果发现其他链表中有满足需求的空闲 chunk 块,就会将其临时分配出去,以缓解当前内存分配压力。如果其他链表中没有满足需求的空闲 chunk 块可以借用,就会直接调用
malloc()
继续分配内存空间。
- 当某一特定字节大小的内存块分配失败时,二级空间配置器会遍历所有字节大小的自由链表,查找是否有可用的空闲 chunk 块可暂时 “借用”。如果发现其他链表中有满足需求的空闲 chunk 块,就会将其临时分配出去,以缓解当前内存分配压力。如果其他链表中没有满足需求的空闲 chunk 块可以借用,就会直接调用
支持 OOM 回调机制
- 如果其他自由链表中没有空闲的 chunk 块可以借用,且调用
malloc()
分配内存失败,那么二级空间配置器将尝试调用用户预设的oom_handler
回调函数(可以用于释放用户自定义的内存资源),以应对malloc()
执行失败的情况(在死循环中处理)。若用户未设置oom_handler
回调函数,则直接抛出 OOM(Out Of Memory)异常,提示内存分配失败,确保程序行为的可控性和安全性。
- 如果其他自由链表中没有空闲的 chunk 块可以借用,且调用
SGI STL 一级和二级空间配置器的关系
SGI STL 内存池的的核心源码文件
源码移植
这里将移植 SGI STL 二级空间配置器中的内存池代码,即单独抽出内存池的代码以复用,其中移植的代码主要位于 SGI STL 的 stl_alloc.h
头文件中。
核心源码
allocator.h
头文件
1 |
|
测试代码
main.cpp
源文件
1 |
|
- 测试代码运行的输出结果
1 | 3 4 1 8 8 5 10 1 8 8 |