翻译:深入理解内存池 Boost Pool in More Depth
深入理解内存池
本文是 Boost 官网中 Pool in More Depth 1.72.0 版本1的中文翻译。
使用内存池的基本理由
自 20 世纪 60 年代以来,动态内存分配已经成为大多数计算机系统的一个基础部分……2
每个人都会用到动态内存分配。如果你曾调用过 malloc 或者 new,那么你就用过动态内存分配。大多数程序员都倾向于认为堆是一个“神奇的袋子”:我们向它请求内存,然后它就像变魔术一样创造出来一些给我们。有时候我们会因为堆并不神奇而陷入麻烦。
堆是受限的。即便是在拥有大量可用虚拟内存的大型系统中(也就是说不是嵌入式系统),堆也是有上限的。谁都知道堆有个物理上限,但实际上还有个更加虚拟的上限,这个上限是由于你使用了虚拟内存而产生的。这个虚拟上限相比与物理上限来说更接近你的程序,尤其是你在一个多任务系统中运行程序时。因此,当在一个大型系统中编程时,最好让你的程序使用尽可能少的内存,并且尽可能快地释放它们。当使用嵌入式系统时,程序员通常没有多余的内存来挥霍。
堆也是复杂的。它不得不满足任何类型和任意大小的内存请求,并且要做的很快。常见的内存管理方法时将内存分割成若干份,并且让他们根据大小排列在一种树型或链形数据结构中。加上其他的一些因素,比如局部性和预估生命周期,堆迅速变得非常复杂。事实上,问题如此复杂导致我们找不到完美的动态内存分配解决方案。下面的两张图显示了大多数内存管理方案是怎么做的:对于任何内存块来说,内存管理器使用这个内存块的一部分来维持其内部树状或线性结构。即便当一个内存块已经被程序分配了,内存管理器也必须在其中储存一些信息,通常只是这个块的大小。然后当这个块被释放时,内存管理器可以轻松地知道这个块有多大。


动态内存分配通常比较低效
由于动态内存分配的复杂性,从时间或空间的角度来看动态内存分配通常比较低效。大多数内存分配算法在每一个内存块中都储存一些信息,可能是块的大小,也可能是一些关系信息,比如这个块在树或链表中的位置。在被程序占用的内存块中,这些头部域通常使用一个机器字。一个显然的缺点是在小块内存被动态分配时出现的。举例来说,当 int 类型的变量被动态分配时,算法同样会自动地在内存块中保留头部域,然后我们就浪费了一半的内存。当然,这是个最坏情形。然而现代程序经常在堆上分配小块内存,这就让这个问题越来越显著。Wilson 和他的合作者表示平均情形下内存支出是 10% 到 20%3。这个支出会随着程序中使用更小的对象而变大。正是这些内存支出使得程序更加接近上文中提到的虚拟上限。
在大型系统中,这个内存支出并不是个大问题(和为了解决这个问题我们需要浪费的时间比),因此这个问题通常会被忽略。然而有些情况下我们会在时间攸关的算法中使用大量的内存分配或释放,这些情况下系统实现的内存分配器就比较慢了。
简单隔离储存同时解决了两个问题。基本所有的内存开支都被小除了,并且内存分配活动可以在一个(平摊了的)常数时间内完成。然而这种做法牺牲了一般性;简单隔离存储只能分配同样大小的内存块。
简单隔离存储
简单隔离存储是 Boost Pool 库的基本想法。简单隔离存储时最简单,甚至可能是最快的内存分配释放算法。它始于将大块内存分解为固定大小的块(译注:这里的大块对应的原文是 block,块对应的原文是 chunk,中文里只有“块”一个词,于是称 block 为大块,chunk 为小块;后面的翻译中只有“大块内存”指的是 block,其余均为 chunk,如“内存块”、”块“和“小块内存”都是后者)。这一大块内存是从哪来的在开始实现它之前不用理会。一个内存池就是一个像这样使用简单隔离存储的对象。

任何一个给定的大块内存中每个小块都是同样大的。这是简单隔离存储的基本限制:你不能向这个算法请求一个不同于块大小的内存块。举例来说,你不能向一个储存整数的内存池请求一个字符,同样也不能向一个储存字符的内存池请求一个整数。(假设整形变量和字符变量的大小不一样)
简单隔离存储通过在它内部插入一个记录未被使用的小块的可利用空间表来工作。比如:

通过建立块之间的可利用空间表,每个简单分割存储算法中只有一个指针(指向表头)的额外开销。可供进程使用的内存中没有任何额外开销。
简单分割存储同时非常非常快。在最简单的情况下,内存分配只需要简单地将可利用空间表中的第一个元素抛出,一个常数时间的操作。当表是空的的时候,另外一大块空间会被请求并分配,结果是使用了平摊后的常数时间。内存释放也非常简单,只需要把一块内存放在可利用空间表的表头即可,也是常数时间。然而,更复杂的简单分割存储算法可能会使用一个支持排序的可利用空间表,这会使得释放空间操作的时间复杂度提升到线性。

简单分割存储相比系统提供的分配器来说执行更快,并且内存开销更小,但通用性低很多。当很多(非相邻的)小对象需要被动态分配在堆上时,或者需要大量重复分配释放同样大小的对象时使用内存池是非常合适的。
(未完待续)
引用
[1]: Boost, Pool in More Depth, ver. 1.72.0. See https://www.boost.org/doc/libs/1_72_0/libs/pool/doc/html/boost_pool/pool/pooling.html
[2]: Doug Lea, A Memory Allocator. See http://gee.cs.oswego.edu/dl/html/malloc.html
[3]: Paul R. Wilson, Mark S. Johnstone, Michael Neely, and David Boles, Dynamic Storage Allocation: A Survey and Critical Review in International Workshop on Memory Management, September 1995, pg. 28, 36. See ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.ps
浙公网安备 33010602011771号