内存池可以比通用内存分配更有效,但通常只是因为您有关于分配模式的额外信息。也许它们最重要的特性是它们的运行时间是确定性的,尤其是在实时操作系统中。
例如,我曾经编写过一个嵌入式系统,我知道所需的最大分配是 128 字节(以下称为块)。为此,我维护了一组连续的块,并使用映射来确定一个块是否空闲。
它最初是一个位图,但我们最终通过将每个已使用/未使用的标志存储在一个单独的字节中来获得更高的性能。地图的内存使用量是地图的八倍,但是,由于池大小是已知的并且合理有限(一千左右),这还不算太糟糕。而且它让我们不必费力地进行池管理,从而提高了我们的速度。
我们还添加了其他优化,例如存储第一个空闲块,以便我们可以快速找到它。它易于维护,因为:
- 释放低于当前最低值的块只会更新最低值;和
- 分配最低块只会增加最低块 - 虽然这并没有保证它指向一个空闲块,但它仍然会使搜索更快,并避免可能不必要的分配搜索(例如,如果您首先释放的块低于您刚刚分配的块)。
然后,如果您要求超过块大小,它会返回 NULL(这在该系统中从未发生过,但出于偏执,我为它编写了代码以防万一)。如果您要求的东西可以放入一个块中,那么无论如何您都会得到一个完整的块(但是,当然,您仍然应该只使用您要求的内存,以防我以后想更改块大小或从单独的具有不同块大小的池)。
事实证明,这比当时的通用分配器要快很多,因为它们必须处理不同的请求大小并担心在释放内存时合并连续的空闲块等事情。
但它需要额外的知识,事实上没有分配会超过块大小。
另一种模型是为低于特定大小的请求设置一个池,但如果出现以下任一情况,则恢复为一般分配:
在大多数情况下,这可以让您获得额外的效率(当然取决于您的分配模式),但允许分配超出此范围。它会在每次分配中引入一些额外的工作,因为您需要评估请求大小和池耗尽,但它仍然可能优于一般情况。
顺便说一句,我记得 Java 字符串中有类似的东西(不确定是否仍然如此,我已经有一段时间没有使用 Java了)。字符串对象分配内部有一个缓冲区用于存储 small 字符串,但也可以使用该空间来存储单独分配的字符块的指针(如果它大于内部缓冲区)。这减少了可能是大量小字符串的碎片(和取消引用),但仍允许在需要时使用更大的字符串。
有趣的是,我曾经在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} 的 64 位系统。 32 位系统的块大小设置略有不同,{8, 16, 24, 32, ..., 512}。
对于竞技场内的游泳池,它们是:
- 部分使用,在这种情况下,根据其块大小,它们位于竞技场中的双向链表中。 arena 维护池的空闲列表,每个块大小一个。
- 未使用,在这种情况下,它们位于一个空闲的池列表中,能够为任何大小的请求提供服务(不过,一旦锁定,这就是它们被限制的块大小)。李>
- 已满,在这种情况下,它们将无法访问,除非取消分配。
请记住,这三个状态之间的转换都会导致池从一个列表移动到另一个列表。
我不会详细说明,因为你的脑袋可能会爆炸,就像我的一样 :-)
简而言之,CPython 对象分配总是针对一个特定块大小,最小的一个大于或等于您需要的大小。这些来自提供单个块大小的池(一旦锁定)。这些池存在于为防止碎片化而进行了高度优化的领域中。并且可以根据需要创建竞技场。
我只想说,这就是我的小实验没有改进 CPython 的原因:它已经以一种相当复杂但高效的方式进行内存池,而我试图拦截 malloc 的尝试完全没有效果。
我的开场白可以更有效率“但通常只是因为你有关于分配模式的额外信息”得到了那本书的评论的支持:
大多数内存分配请求都很小且大小固定。因为PyObject 是 16 字节,PyASCIIObject 是 42 字节,PyCompactUnicodeObject 是 72 字节,PyLongObject 是 32 字节。
(a)CPython Internals 如果你有兴趣,除了我喜欢关于工作原理的优秀技术书籍之外,我没有任何隶属关系。