在内存池中查找下一个可用区块

Find next available chunk in a memory pool

本文关键字:用区 下一个 查找 内存      更新时间:2023-10-16

所以,我花了一些时间在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是池中块的数量。

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

任何想法都会受到赞赏,因为这是我第一次以这种方式直接与记忆打交道。谢谢

与其使用手工编制的链表,不如使用std::list(尤其是与自定义分配器一起使用)。不易出错,而且可能优化得更好。

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

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

如果区块大小固定,那么只需将第一个可用区块从空闲列表移动到已分配列表即可实现分配,无需搜索。要解除分配区块,您仍然需要在分配的列表中找到它,这意味着您需要将T*映射到列表中的条目(例如,执行搜索),但解除分配的行为只是将条目从一个列表移动到另一个列表。

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

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

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

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

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

如果你担心内存开销,你可以为每个bin只附加一次每个chunk。也就是说,即使块具有128个可用的1页区域(最大程度地分段),仓1仍将仅链接到该块一次并重用它128次。

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

请注意,无论哪种方式,如果你不想让每个区块内的可用空间变得碎片化,你都需要一种快速的方法来从列表中的垃圾箱中删除区块,这意味着使用双链接列表。显然,这会增加额外的内存开销,但它可能仍然比对整个列表进行定期的可用空间碎片整理更可取。