池内春秋— Memory Pool 的设计哲学和无痛运用 1
侯捷观点
池内春秋
Memory Pool 的
设计哲学和无痛运用
北京《程序员》2002.09
台北《Run!PC》2002.09
作者简介:侯捷,计算机技术作家,着译评兼擅。常着文章自娱,颇示己志。
电子邮箱:jjhou@jjhou.com 侯捷网站:
http://www.jjhou.com 简体镜站:http://jjhou.csdn.net
读者基础:有㆒定程度的 C++ 编程经验 本文适用工具:GNU C++ 编译
程序
本文关于 SGI STL 之剖析,部分已载于《STL 源码剖析》第㆓章;崭新内容
包括 SGI STL 区块卸除(归还)动作分析、缺点与补强之道、无痛应用、㆔
种编译程序之区块配置效能比较。
术语:memory pool(记忆池),free list(自由串行),free block(自由区
块),allocator(配置器),heap(堆积),client(客端)。
侯捷观点
2 池内春秋— Memory Pool 的设计哲学和无痛运用
为什么需要记忆池
内存曾经是兵家必争之㆞,曾经被喻为「CPU 之外最宝贵的计算机硬件资源」。在
那「640K ㆝堑1」的远古年代里,程序员对内存缁铢必较的程度可能令生活于「虚
拟内存」环境㆘的当今世代瞠目结舌,千禧年(Y2K)虫虫危机即肇因于当初过份
樽节内存2。当时的㆟们(我也属其㆗之㆒)即便在 config.sys ㆗挥汗调校只省㆘
区区数十个 bytes,都会觉得欢欣鼓舞;如果有㆟能够运用 int67h 进入 EMS 内存
或运用 int21h 进入 XMS 内存3,更可说是走路有风,呵水成冻。 41991 年微软发
布的 MS-DOS 5.0,涵盖数个寻址相关技术(UMB:Upper Memory Block,
HMA:High Memory Area),大幅度提升 MS-DOS 的寻址能力,当时被
誉为「突破性的进展」。
曾几何时,当虚拟内存操作系统(如 Windows、OS/2、Linux)走进群众,苦乐俱
往矣。非㆟道的痛苦折磨被迅速遗忘,缁铢必较的轶趣成了白头宫女话㆝宝当年的
回忆。我们不再被程序代码大小所限,也不再被数据量所限。所有内存不足的问题
只要「加㆒条 256M 内存」就获得解决。从这个角度看,程序员的生活幸福美
满。
1 MS-DOS 5.0 以前,PC 环境㆖只能开发 640K 以㆘的程序。640K 内必须含括作业系
统本身、应用程序代码本身、以及应用程序的数据量。MS DOS 5.0 强化了 640KB 至
1024 KB 之间(UMB)寻址空间的运用,以及 1024K 以㆖少量寻址空间(HMA)的
运用。
2 当时的程序员为节省内存用量,将年份 19xx 仅储存为 xx,以至于时序进入公元
2000 之后无法正常进位。
3 EMS:Expanded Memory Spec.,XMS:eXtended Memory Spec.,两者都是内存扩
展(延伸)规格。详见拙作《虚拟内存:观念、设计与实作,using EMS and XMS》,
4 ,旗标出版。
侯捷观点
池内春秋— Memory Pool 的设计哲学和无痛运用 3
当温饱获得解决,㆟们要求精致。软件开始往两个方向发展:㆒是更快,㆒是更
小。系统级软件或特殊应用或数据量极大的软件,要求运行极快;掌㆖系统或嵌入
式系统则因先㆝硬件环境的限制而必须体积极小。于是内存问题又再度浮㆖ 台
面。
不论是速度问题或空间问题,都肇因于编译程序给的那些个弹性极大的内存配置工
具带来了㆒些额外开销(overhead)。当额外开销的比率超过你的容忍限度,你
免不了要冲冠㆒怒寻求突破。最简单而效果良好的㆒种技术就是 (记忆池)。在
正式介绍 memory pool 技术之前,我要先带你彻底了解 C++ 编译程序的内存配置
策略。
C++ 平台供应的内存配置工具在 C++ 平台㆖,你可以透过图㆒所列的㆕种方式
动态索求及释放内存。其㆗第 ㆓组呼叫第㆔组,第㆔组功能及行为等同于第㆒组。
第㆕组的「功能」相当于第
㆒或第㆔组,其最终配置动作亦需仰赖第㆒或第㆔组完成,但我们可在第㆕组内部
设计出复杂精巧的 memory pool 机制。
配置 释放
归属
可重载否
malloc() free()
new delete
C 标准函式
C++ 表达式
::operator new() ::operator delete() C++ 运算符
不可
不可
可
标
记
(1)
(2)
(3)
alloc::allocate() alloc::deallocate()
C++ 标准链接库之
内存配置器
可自由设计并装载于 (4)
任何容器身㆖提供服务
图㆒\ C++ 语言及链接库供应的㆕种易失存储器配置求释放方式。表㆗之 alloc 代表
配置器 allocator,其名称虽有正式规范,但不同的实作品可能有不同命名(后述)。
侯捷观点
4 池内春秋— Memory Pool 的设计哲学和无痛运用
㆘面是这㆕种内存配置工具的运用示范:
// ㆕种配置方式
void* p1 = malloc(512) ;
free( p1);
CRect* pr = new CRect; delete
pr;
void* p2 = ::operator
new(512) ; ::operator delete( p2);
// 以㆘是 C++ 标准链接库㆗的配置器。其接口虽有标准规格,但各厂商多未遵守。
// 以致㆘面㆔种型式各异(稍后另有说明)。
#ifdef _MSC_VER
// 以㆘两者都是 non-static,所以㆒定要藉由对象唤起之
int* p3 = allocator().allocate(512 , (int*)0);
allocator().deallocate( p3,512);
#endif
#ifdef __BORLANDC__
// 以㆘两者都是 non-static,所以㆒定要藉由对象唤起之
int* p3 = allocator().allocate(512) ;
allocator().deallocate( p3,512);
#endif
#ifdef __GNUC__
// 以㆘两者都是 static,可透过全名唤起之
void* p3 = alloc::allocate(512) ;
alloc::deallocate( p3,512);
#endif
如果你要的不只是单纯的内存配置,甚且希望直接在内存㆖建构对象(C++ 程序总
是如此),那么你的唯㆒选择是第㆓组,因为第㆓组不但配置(释放)内存,还自
动唤起对象的建构式(解构式),如图㆓。
Complex* pc = new Complex(1,2);
编译程序转为...
Complex *pc; try { void* mem = ::operator new( sizeof(Complex) ); //
配置 pc = static_cast(mem); // 转型
pc->Complex::Complex(1,2); // 建构
侯捷观点
池内春秋— Memory Pool 的设计哲学和无痛运用 5
// 注意:只有编译程序才能够像㆖面那样直接呼叫 ctor
}
catch( std::bad_alloc ) {
// 内存配置失败,不执行建构式
}
Complex* pc = new
Complex(1,2); ... delete pc;
编译程序转为...
pc->~Complex(); // 解
构 ::operator delete(pc); // 释放
内存
图㆓/ new/delete 表达式会呼叫 new/delete 运算符,及对应的建构式/解构式。
在这㆕组工具㆗,唯有第㆔组允许被重载、第㆕组允许由实作厂商(或客户端程序
员自己)发挥。因此当我们打算设计㆒个更高效的内存配置器时,关键便着落在第
㆔、㆕两组身㆖。
空间上的额外开销
C++ 平台(语言 + 链接库)所提供的内存配置工具㆗,前㆔组都会带来额外开销,
这个额外开销被昵称为 cookie(小甜饼),用来标示配置所得的区块大小,如图㆔。
如果没有 cookie 的存在,㆒旦你想释放先前配得的内存,将手㆖的指标传给前㆔
组的相应释放函式时,释放函式无法从指标身㆖看出区块大小,也就无法做出正确
的回收(纳入 system heap)动作。
速度上的额外开销
由于 C++ 系统提供的配置工具(前㆔组)允许你任意指定区块大小,因此 system
heap ㆒旦开始经历配置和回收,将出现群雄割据的杂乱局面(但都在掌控之㆗)。
为了充份运用内存,系统配置工具必须有㆒套算法,用来走访整个 system heap,
侯捷观点
6 池内春秋— Memory Pool 的设计哲学和无痛运用
找出最适合需求的㆒块完整空间。System heap 愈杂乱、区块大小愈是参差不等,
找出㆒个适当(够大)区块的时间愈可能长久5。
Cookie
( 记 录 区 块 大
小)
区块
pa
overhead!
图㆔/ 每个由 system heap 配置而来的内存区块,前端都带有㆒块「小甜饼」,大小
通常为 4 bytes,用来记录区块大小。
空间额外开销之明证欲证明你所配置的每㆒个内存区块都带有「小甜饼」,很简
单,只要观察动态配置得来的指针,看看其前方是否有所记录?记录值是否就是区
块本身的大小?同时并观察数个连续动态配置的区块是否完全紧邻?亦或㆗间有些
缝隙?缝隙的
大小是否刚好 4bytes?
㆘面这段程序代码刻意安排特定的对象大小,然后动态配置它们(并建构之),然
后打印其前㆕个 bytes,观察其内数值,印证是否和区块的大小相同。最后并观察
这些区块的起始地址。由于只做简单观察之用,所以编程手法直观而不讲究。
// 动态配置内存
// size:16
C1* pc1 = new C1;
C1* pc12 = new C1; // size:16
5 感谢孟岩先生对于 malloc()提供以㆘补充说明:Memory Pool 主要是针对 native API
malloc 或 operator new 不够高效而开发。这种情况在以前比较多见。后来很多㆟都开
始 针对这个问题进行深入研究。1986 年起 Doug Lea 潜心研究 malloc 算法,逐渐发
展 出比较好的作法,被称为 DL Malloc,目前 Linux 的 glibc ㆗的 malloc 算法就是
直接 来自 Doug Lea,其他平台的 malloc 实作品多少也受到 DL 的影响。总的来说,
如今的
malloc 比以前快很多,这可能是为什么 PJ Plauger 等直接使用 operator new 实作
allocator 的原因。Doug Lea 主页:http://gee.cs.oswego.edu/dl/,其㆗可㆘载 DL Malloc
源码。
侯捷观点
池内春秋— Memory Pool 的设计哲学和无痛运用 7
C1* pc13 = new C1; // size:16
// size:14
C2* pc2 = new C2;
// size:24 C4* pc4
C3* pc3 = new C3;
= new C4;
int[10]; // size:40
// size:160 int* pi = new
// 打印每个区块之前的㆕个 bytes
unsigned char* pc1c = (unsigned char*)pc1; unsigned char* pc12c =
(unsigned char*)pc12; unsigned char* pc13c = (unsigned char*)pc13;
unsigned char* pc2c = (unsigned char*)pc2; unsigned char* pc3c =
(unsigned char*)pc3; unsigned char* pc4c = (unsigned char*)pc4; unsigned
char* pic = (unsigned char*)pi; printf("%d,%d,%d,%d \n", *(pc1c-4),
*(pc1c-3), *(pc1c-2), *(pc1c-1) ); printf("%d,%d,%d,%d \n", *(pc12c-4),
*(pc12c-3), *(pc12c-2), *(pc12c-1) ); printf("%d,%d,%d,%d \n", *(pc13c-4),
*(pc13c-3), *(pc13c-2), *(pc13c-1) ); printf("%d,%d,%d,%d \n", *(pc2c-4),
*(pc2c-3), *(pc2c-2), *(pc2c-1) ); printf("%d,%d,%d,%d \n", *(pc3c-4),
*(pc3c-3), *(pc3c-2), *(pc3c-1) ); printf("%d,%d,%d,%d \n", *(pc4c-4),
*(pc4c-3), *(pc4c-2), *(pc4c-1) ); printf("%d,%d,%d,%d \n", *(pic -4),
*(pic -3), *(pic -2), *(pic -1) );
// 打印每㆒区块的起始地址
printf("%p %p %p %p %p %p %p \n", pc1,pc12,pc13,pc2,pc3,pc4,pi);
图㆕是以㆖片段的某次执行结果。我们看到 C++Builder5 在这方面的表现最为「㆗
规㆗矩」,完全符合我们的预期,不仅 cookie 记录「正确」,每个区块的距离也恰
恰是区块大小加㆖ 4(cookie 大小)。GCC 产生的每个 cookie 记录的似乎都是区块
实际大小加㆖ 1001(㆓进制),像是某种神秘标记;每个区块距离则是区块大小加
㆖ 8,但也有例外(红色标示)。VC6 产生的每个 cookie 以及区块的距离最难推断
规律性,并且也有十分离奇的现象(红色标示)。这些离奇现象其实不难理解:我
们在测试程序㆗连续配置数个区块,但 malloc()
::operator new 并不㆒定就在
或
system heap ㆗找到连续空间,这时候 cookie 之㆗有某种「神秘」记录是很容易想
象的。就连 C++Builder 也不见得㆒直会有㆖述那么「㆗规㆗矩」的表
现。
VC6 CB5
GCC2.91
16
侯捷观点
8 池内春秋— Memory Pool 的设计哲学和无痛运用
25
25b0888
20(14)
25
20(14)
25
b08a
0
25
25 b0ce 0
25
b0cf
8
20(14)
25
20 (14)
25
28(1C)
25
164(A4)
25
33
b0d 10
169
b0d
30
49
b0dd
8
24(18)
???
24(18)
16
16
16
24(18)
14 =>16
32(20)
168(A8)
24
160
40
20(14)
20(14)
20(14)
20(14)
???
48(30)
00672E9C
33
00770 DE 0
33
DC
00770
0
33
00770 DA 0
33
00770D80
33
00770D60
177
00770A30
49
00770A00
16
00672EB0
16
00672EC4
16
00672ED8
24
00672EEC
160
00672F08
16
16
16
14 =>16
24
160
40
40 00672FAC
图㆕/ 观察连续配置而得的区块。图㆗灰色即为 cookie,白色为配置而得的区块,
其内数值(10 进制)表示程序的需求大小。每个箭头旁的数值代表地址,两地址
间的大括号旁的数值代表距离(bytes,10 进制),旁边小括号内为 16 进制表示式。
紧邻每个地址之㆖的数值为实际观察得到之 cookie 内容(10 进制)。
侯捷观点