【问题标题】:Find next available chunk in a memory pool在内存池中查找下一个可用块
【发布时间】:2017-01-01 23:18:42
【问题描述】:

所以,我一直在花一些时间在 C++ 中实现一个内存池类。除了一路上的一些小问题,一切都还算顺利。但是,当我今天尝试通过首先使用内存池分配 1000 个块然后将其与使用 new 进行比较来测试它时,我实际上在使用时接近了差三倍的性能(以纳秒为单位)内存池。我的分配方式是这样的:

template <class T> T* MemPool<T>::allocate()
{
    Chunk<T>* tempChunk = _startChunk;

    while (tempChunk->_free == false)
    {
        if (tempChunk->_nextChunk == NULL)
            throw std::runtime_error("No available chunks");

        tempChunk = tempChunk->_nextChunk;
    }

    tempChunk->_free = false;
    return &tempChunk->object;
}

我从池中的第一个块开始,并在池的链表中进行搜索,直到找到一个空闲块,或者到达池的末尾。现在,池越大,搜索所需的时间就越长,因为搜索的时间复杂度为 O(n),其中 n 是池中的块数。

所以我很好奇是否有人对如何改进分配有任何想法?我最初的想法是使用两个链表,而不仅仅是一个,其中一个包含空闲块,另一个包含分配的块。当要分配一个新块时,我只需将第一个提到的链表中的第一个元素移动到分配的链表中。据我所知,这将消除在分配时进行任何搜索的需要,而只留下需要搜索以找到正确块的解除分配。

感谢任何想法,因为这是我第一次以这种方式直接使用记忆。谢谢!

【问题讨论】:

  • 你的块大小都一样吗?

标签: c++ memory memory-management pool


【解决方案1】:

与使用手工制作的链表相比,使用std::list 可能更有效(特别是如果您将它与自定义分配器一起使用)。更不容易出错,并且可能得到了更好的优化。

使用两个列表可以简化很多。无需在列表本身中跟踪一个块是否空闲 - 因为这将由该块所在的列表指定(所需要的只是确保一个块不会以某种方式出现在两个列表中)。

您当前的实现意味着您必须在分配和解除分配时遍历链表。

如果块是固定大小的,那么分配将简单地通过将第一个可用块从空闲列表移动到已分配列表来实现 - 无需搜索。要释放一个块,您仍然需要在已分配的列表中找到它,这意味着您需要将 T* 映射到列表中的条目(例如执行搜索),但随后释放的行为将很简单将条目从一个列表移动到另一个列表。

如果块的大小可变,则需要做更多的工作。分配将需要在分配时找到至少是请求大小的块。过度分配(分配比需要更大的块)将使分配和释放在性能方面更有效,但也意味着可以从池中分配更少的块。或者,将一个大块(来自空闲列表)分成两部分,并在两个列表上放置一个条目(代表已分配的部分,以及未分配的部分)。如果这样做,在解除分配时,可能需要合并内存中相邻的块(实际上,实现池中空闲内存的碎片整理)。

您需要决定是否可以从多个线程中使用该池,并使用适当的同步。

【讨论】:

  • 还有一点要提的是,当你进行碎片整理时,指向所分配内存的指针会失效,所以你需要对指针进行重定位。解决方案可能是使用智能指针或句柄
  • 我只是指池中已释放(未使用)空间的碎片整理。如果对分配的内存进行碎片整理,您所说的将是正确的。
  • 太好了,这很有道理。谢谢!
【解决方案2】:

使用固定数量的大小箱,并使每个箱成为一个链表。

例如,假设您的 bin 只是系统页面大小的整数倍(通常为 4KiB),而您使用 1MiB 的块;那么你有 1MiB/4KiB = 256 个垃圾箱。如果 free 使块中的 n 页区域可用,则将其附加到 bin n。在分配 n 页区域时,遍历从 n 到 256 的 bin,然后选择第一个可用的块。

为了最大限度地提高性能,请将 bin 与位图相关联,然后从位 n-1 扫描到位 255 以找到第一个设置位(使用编译器内在函数,如 __builtin_clz 和 _BitScanForward 计算前导或尾随零)。由于 bin 的数量,这仍然不是 O(1),但非常接近。

如果您担心内存开销,您可以为每个 bin 仅附加每个块 一次。也就是说,即使一个块有 128 个可用的 1-page 区域(最大碎片),bin 1 仍然只会链接到该块一次并重用它 128 次。

为此,您必须在每个块内将这些区域链接在一起,这意味着每个块还需要存储大小箱的列表 - 但这可以提高内存效率,因为最多只有 256 个有效偏移量在每个块内,而列表需要存储完整的指针。

请注意,无论哪种方式,如果您不希望每个块内的可用空间碎片化,您需要一种快速的方法从列表中的 bin 中删除块 - 这意味着使用双向链表。显然这会增加额外的内存开销,但它可能仍然比对整个列表进行定期可用空间碎片整理更好。

【讨论】:

    猜你喜欢
    • 2010-11-27
    • 1970-01-01
    • 1970-01-01
    • 2011-01-02
    • 2014-01-10
    • 2016-11-28
    • 1970-01-01
    • 1970-01-01
    • 2021-01-16
    相关资源
    最近更新 更多