首页 > 解决方案 > 如果内存池比 malloc 快,为什么 malloc 不在幕后使用它们?

问题描述

我一直听说内存池在分配内存时可以显着提高性能。那么为什么传统的 malloc 实现不以某种方式使用它们呢?

我知道部分原因是内存池使用固定大小的内存块,但似乎有些没有,他们唯一需要的就是提前获取一些额外的内存。有没有一种方法可以将它们充分概括用于此类目的?

标签: c++cmallocmemory-pool

解决方案


内存池可能比通用内存分配更有效,但通常只是因为您有关于分配模式的额外信息。也许它们最重要的特性是它们的运行时间是确定性的,例如在实时操作系统中尤其重要。

例如,我曾经编写过一个嵌入式系统,我知道需要的最大分配是 128 字节(以下称为块)。为此,我维护了一组连续的块,并使用一个映射来决定一个块是否空闲。

它最初是一个位图,但我们最终只需将每个已使用/未使用的标志存储在一个单独的字节中即可获得更高的性能。地图的内存使用量是地图的八倍,但是,由于池大小是已知的并且合理有限(一千左右),这还不算太糟糕。它给了我们更快的速度,因为我们不必费力地进行池管理。

我们还添加了其他优化,例如存储第一个空闲块,以便我们可以快速找到它。它易于维护,因为:

  • 释放低于当前最低值的块只会更新最低值;和
  • 分配最低块只会增加最低块 - 虽然这并不能保证它指向一个空闲块,但它仍然会使搜索更快,并避免可能不必要的分配搜索(例如,如果你首先释放了一个块低于你刚刚分配的那个)。

然后,如果您要求超过块大小,它会返回 NULL(这在该系统中从未发生过,但出于偏执,我为它编写了代码以防万一)。如果您要求的东西可以放入一个块中,那么无论如何您都会得到一个完整的块(但是,当然,您仍然应该只使用您要求的内存,以防我以后想更改块大小或从单独的具有不同块大小的池)。

事实证明,这比当时的通用分配器要快得多,因为它们必须处理不同的请求大小并担心在释放内存时合并连续的空闲块等事情

但它需要额外的知识,即没有分配会超过块大小的事实。


另一种模型是为低于特定大小的请求设置一个池,但如果出现以下任一情况,则恢复为一般分配:

  • 您请求的块超出了块大小;或者
  • 池目前已用尽。

在大多数情况下,这可以让您获得额外的效率(当然取决于您的分配模式),但允许分配超出此范围。它在每次分配中引入了一些额外的工作,因为您需要评估请求大小和池耗尽,但它仍然可能优于一般情况。

顺便说一句,我记得 Java 字符串中有类似的东西(不确定是否仍然如此,我已经有一段时间没有使用 Java)了。字符串对象分配内部有一个缓冲区用于存储字符串,但也可以使用该空间来存储单独分配的字符块的指针(如果它大于内部缓冲区)。这减少了可能是大量小字符串的碎片(和取消引用),但如果需要,仍然允许使用更大的字符串。


有趣的是,我曾经在CPython源代码中尝试过一个实验,看看内存池是否可以提高性能,特别是考虑到那里进行的内存分配数量。它使用类似于上面给出的策略,优先从池中分配,但如果请求的大小超出块大小或池已用尽,则恢复为原始策略。

再一次,它有讨论的优化,然后是一些。例如,最后一个释放的块被缓存,因此它可以立即分发而无需对池进行任何搜索,以尝试加速many-times(single-free-then-allocate)模式。

然而,即使有各种优化、池和块大小,它似乎对我编写的一些测试代码的性能没有实质性的影响,这让我相信 CPython 中使用的实际分配器已经相当不错了。


而且,刚读完我几周前买的这本好书(a),我现在知道为什么我没有取得任何进展。

事实证明,CPython已经进行了大量优化,包括内存池的使用。“内存管理”一章更详细,但它基本上只使用普通分配器(原始域)来获取大块(> 256K)或特定的非对象相关内存。

所有对象,而 Python 几乎就是所有对象 :-),都来自对象域(除了一些遗留的东西)。

对于此域,它维护自己的堆并分配大小以匹配系统页面大小的区域,mmap如果支持则使用以减少碎片。所有使用过的 arena 都保存在一个双向链表中,空的 arena 保存在一个单链空闲列表中。

在每个 arena 中,创建 4K 个池(因此每个 arena 64 个),一个池只能提供一种大小的分配,当从该池请求第一个分配时锁定。例如,1-16 字节的请求将从服务 16 字节块的池中获得 16 字节的块,33-48 字节的请求将来自服务于 48 字节块的池。

请注意,这是针对块大小为{16, 32, 48, ..., 512}. 32 位系统的块大小集略有不同,{8, 16, 24, 32, ..., 512}.

对于竞技场内的游泳池,它们是:

  • 部分使用,在这种情况下,根据它们的块大小,它们存在于竞技场中的双向链表中。arena 维护池的空闲列表,每个块大小一个。
  • 未使用,在这种情况下,它们存在于池的空闲列表中,能够服务于任何请求大小(尽管一旦锁定,这就是它们被限制的块大小)。
  • 已满,在这种情况下,它们将无法访问,除非取消分配。

请记住,这三个状态之间的转换都会导致池从列表移动到列表。

我不会再详细介绍了,因为你的头可能会爆炸,就像我的几乎一样:-)

简而言之,CPython 对象分配总是针对特定的块大小,最小的一个大于或等于您需要的大小。这些来自提供单个块大小的池(一旦锁定)。这些池存在于为防止碎片化而进行了高度优化的领域中。并且可以根据需要创建竞技场。

可以说,这就是我的小实验没有改进 CPython 的原因:它已经以一种相当复杂但高效的方式进行内存池,而我的拦截尝试malloc根本没有用。

那本书的评论支持我的开场白,即池化内存可以更有效“但通常只是因为你有关于分配模式的额外信息”:

大多数内存分配请求都很小并且大小固定。因为PyObject是16字节,PyASCIIObject是42字节,PyCompactUnicodeObject是72字节,PyLongObject是32字节。


(a) CPython Internals如果您有兴趣,除了我喜欢关于事物如何工作的优秀技术书籍这一事实之外,我没有任何从属关系。


推荐阅读