内存池概念(Pool Concepts)
网页链接:http://www.boost.org/libs/pool/doc/concepts.html
“大概从1960年起,动态内存分配机制就已经成为多数计算机系统的基础组成部分了……”
每个人都分配过动态内存――只要你曾经调用过malloc函数或是new运算符。大部分程序员都试图将heap当成一个“魔法口袋”:当我们向它寻求内存,它会耍魔术般变出一些内存来。但是heap并不是魔术师,所以我们常常遭遇麻烦。
heap并不是毫无限制的。即便是在拥有大量可用虚拟内存的大型系统之上,也是会存在某种限制。每个人都知道物理内存限制,但更微妙的是“虚拟”限制,也就是当你的程序(或者是整个系统)使用虚拟内存的时候运行速度会变慢。这个问题比物理内存限制更贴近于程序,特别是把它放在一个多任务系统上运行的时候。因此,尽可能地让大型系统上的程序按需求来索取资源并且及时地释放资源,是一个不错的主意。尤其当工作在嵌入式系统上时,程序员没有多余的内存可供浪费。
heap变得愈加复杂了。它必须确保能够处理各种大小、各种类型的内存分配请求,还得处理得足够快。通常的内存管理方法是将内存分割成好几块,使用树或者链表这样的结构及其上的排序算法来保持这些内存块“以大小为序”排列的状态。考虑更多的因素,比如存放位置及生存期,heap会快速地持续复杂下去。因此,并不存在一个用于解决动态内存分配的、“完美”的解决方案。下图描述了大部分内存管理器的工作原理:对于每一个大块内存,它都使用一小部分内存来维持其内部树结构或链表结构。即使是当一个“块状内存”被分配给程序之后,内存管理器也必须“记录”与它有关的适当信息――通常只是块状内存的大小。然后,内存管理器可以方便地了解即将归还的内存块的尺寸。
未分配的内存块
不受进程控制的内存
在内存分配器算法的内部使用的内存(通常是8~12字节)
未使用的内存
不受进程控制的内存
图一
已分配的内存块(被程序使用)
不受进程控制的内存
在内存分配器算法的内部使用的内存(通常是4字节)
被程序使用的内存
不受进程控制的内存
图二
正是由于这样的复杂度,才导致了内存管理器在时间和/或空间方面的低效率。绝大部分的内存分配算法都为每个内存块保存某种格式化数据,诸如区块大小或者相关的信息,区块在数据结构中的位置等等。在程序中为被使用的内存块设置占用一个机器字的“头部字段”是很常见的。明显的问题产生于许多小对象需要动态地进行分配的时候。例如,当整型变量经常被分配的时候,算法同样会为头部字段保留空间,这将导致50%的内存浪费。当然这只是最坏情况下的假设。即使如此,很多现代的程序仍然在heap上分配小对象;同时也使得问题越来越明显。Wilson等人描述了一种平均情况下的内存开销,大概是从10%到20%。随着程序分配更多的小对象,这个开销会变得越来越大。就是这个开销使得程序更易受虚拟内存限制的影响。
在大型系统中内存开销并不是一个很突出的问题(如果与内存管理工作所需的时间成本相比较的话),因此经常被忽略掉。但是,经常会遇到“时间效率至上的算法需要频繁地进行小对象的大量分配操作和/或释放操作”这种情况;而在这种情况下,系统提供的内存分配器通常工作得很慢。
“简易隔离式存储”方案正是为解决这两个问题而产生的。使用这种方案可以将几乎所有的内存开销都削减至最小,同时分配操作只占用极少量的时间。但是这种做法会丧失些许的通用性――简易隔离式存储仅仅是按一个单一的尺寸来分配内存块。
简易隔离式存储器(Simple Segregated Storage)
“简易隔离式存储”是Boost内存池库使用的基本技术。它是非常简单的、有可能是最快速的内存分配/释放算法。其工作原理是:先将一个memory block按固定的大小分割成数个chunk。除非已经到了实现期,否则不用关心这个block从何而来。内存池则是使用简易隔离式存储技术的某些对象。如图:
内存区块,分割成数个chunk
不受进程控制的内存
0号块
1号块
2号块
3号块
不受进程控制的内存
图三
每一个从内存区块中分割出来的chunk都拥有相同的大小。这是简易隔离式存储的基本限制:你不能请求获取不同大小的chunk。例如不能向整型内存池请求字符型大小的内存块,也不能向字符型内存池请求整型大小的内存块(假定字符型与整型的尺寸相异)。
简易隔离式存储技术使用一种“自由式链表”结构来管理这些chunk。例如:
内存区块,没有内存块被分配
不受进程控制的内存
0号块; 指向1号块
1号块; 指向2号块
2号块; 指向3号块
3号块;链表的尾部
不受进程控制的内存
图四
使用自由式链表来管理chunk,其开销仅仅是单个的指针变量(指向表中的头元素)。被进程使用内存块本身并没有内存使用开销。
简易隔离式存储方案执行得非常快。最简单的情况下,内存分配仅仅是将第一个块从链表中移出的常数时间级操作。当链表为空时,另一个内存区块将被获取与分割,这也仅仅是一个常数时间级操作。反过来,内存释放就如同将内存块加到链表头部那般简单。但是,很多复杂的简易隔离式存储应用要求使用排过序的链表,这将导致释放操作呈现线性复杂度。
内存区块,有内存块被分配
不受进程控制的内存
0号块; 指向2号块
1号块(被进程使用)
2号块; 链表的尾部
2号块(被进程使用)
不受进程控制的内存
图五
与系统提供的分配器相比较,简易隔离式存储方案提供了更快的执行效率和更小的内存开销,但失去了通用性。当有大量的小对象被分配在堆上的时候,或者具有相同的尺寸的对象被反复地分配或者释放的时候,就是使用内存池最佳时机。