mcz*_*nek 9 c c++ malloc memory-pool
我一直听说内存池在分配内存时可以显着提高性能..那么为什么传统的 malloc 实现不以某种方式使用它们呢?
我知道部分原因是内存池使用固定大小的内存块,但似乎有些没有,他们唯一需要的是提前获取一点额外的内存。有没有一种方法可以将它们充分概括用于此类目的?
内存池可能比通用内存分配更有效,但通常只是因为您有关于分配模式的额外信息。也许它们最重要的特性是它们在运行时是确定性的,例如在实时操作系统中尤其重要。
举个例子,我曾经编写了一个嵌入式系统,我知道需要的最大分配是 128 字节(以下称为块)。为此,我维护了一组连续的块,并使用地图来确定一个块是否空闲。
它最初是一个位图,但我们最终通过将每个使用/未使用标志存储在单独的字节中来获得更高的性能。该地图的内存使用量是该地图的八倍,但由于池大小已知且合理有限(一千左右),所以这还不算太糟糕。它使我们无需再费力地进行池管理,从而提高了速度。
我们还添加了其他优化,例如存储第一个空闲块,以便我们可以快速找到它。它易于维护,因为:
然后,如果您要求的大于块大小,它会返回 NULL(这在该系统中从未发生过,但是由于偏执,我为它编码以防万一)。如果你要求一些适合一个块的东西,无论如何你都会得到一个完整的块(但是,当然,你仍然应该只使用你要求的内存,以防我以后想改变块大小或从单独的具有不同块大小的池)。
事实证明,这是很多比广义分配器那名左右的时间更快,因为他们不得不应付不同的要求尺寸和担心像释放内存时合并相连空闲块。
但它需要额外的知识,事实上没有分配会超过块大小。
另一种模型是为低于特定大小的请求设置一个池,但在以下情况下恢复为一般分配:
在大多数情况下,这可以让您获得额外的效率(当然,这取决于您的分配模式),但允许分配超出此范围。它会在每次分配中引入一些额外的工作,因为您需要评估请求大小和池耗尽,但它仍然可能优于一般情况。
顺便说一句,我记得在 Java 字符串中有类似的东西(不确定是否仍然如此,我已经有一段时间没有使用 Java 了)。字符串对象分配内部有一个缓冲区用于存储小字符串,但也可以使用该空间来存储单独分配的字符块的指针(如果它大于内部缓冲区)。这减少了可能是大量小字符串的碎片(和取消引用),但如果需要,仍然允许更大的字符串。
有趣的是,我曾经在CPython源代码中尝试过一个实验,看看内存池是否可以提高性能,特别是考虑到那里进行的内存分配数量。它使用了一种类似于上面给出的策略,优先从池中分配,但如果请求的大小超过块大小或池耗尽,则恢复到原始策略。
同样,它进行了讨论的优化,然后是一些优化。例如,释放的最后一个块被缓存,因此它可以立即分发而无需对池进行任何搜索,以尝试加速many-times(single-free-then-allocate)模式。
然而,即使进行了各种优化、池和块大小,它似乎对我编写的一些测试代码的性能没有实质性的影响,这让我相信 CPython 中使用的实际分配器已经相当不错了。
而且,刚刚读完我几周前购买的好书(a),我现在知道为什么我没有取得任何进展。
事实证明,CPython已经进行了大量优化,包括内存池的使用。“内存管理”一章有更多细节,但它基本上只使用普通分配器(原始域)来获取大块(> 256K)或特定的非对象相关内存。
所有对象,Python 几乎都是对象 :-),都来自对象域(除了一些遗留的东西)。
对于这个域,它维护自己的堆并分配大小与系统页面大小相匹配的 arenas,mmap如果支持则使用它来减少碎片。所有使用过的 arenas 都保存在一个双向链表中,空的 arena 维护在单链空闲列表中。
在每个 arena 中,创建了 4K 池(因此每个 arena 64 个)并且一个池只能提供一种大小的分配,当从该池请求第一次分配时锁定。例如,1-16 字节的请求将从提供 16 字节块的池中获得 16 字节的块,33-48 字节的请求将从提供 48 字节块的池中获得。
请注意,这是针对块大小为{16, 32, 48, ..., 512}. 32 位系统的块大小集略有不同,{8, 16, 24, 32, ..., 512}.
对于竞技场内的游泳池,它们是:
请记住,这三种状态之间的转换都会导致池从列表移动到列表。
由于你的头可能会爆炸,我不会再详细介绍了,就像我的一样:-)
简而言之,CPython 对象分配总是针对特定的块大小,最小的一个大于或等于您需要的大小。这些来自提供单一块大小(一旦锁定)的池。这些池存在于为防止碎片而进行了大量优化的领域中。并且可以根据需要创建竞技场。
可以说,这就是我的小实验没有改进 CPython 的原因:它已经以一种相当复杂但有效的方式进行内存池化,而我的拦截尝试malloc一点用都没有。
我的开场陈述是,池化内存可以更有效,“但通常只是因为你有关于分配模式的额外信息”得到了那本书的评论的支持:
大多数内存分配请求都很小且大小固定。因为
PyObject是 16 字节,PyASCIIObject是 42 字节,PyCompactUnicodeObject是 72 字节,PyLongObject是 32 字节。
(a) CPython Internals如果你有兴趣,除了我喜欢关于事物如何运作的优秀技术书籍之外,我没有任何从属关系。