所以,我花了一些时间在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是池中块的数量。
所以我很好奇是否有人对如何改善分配有任何想法?我最初的想法是使用两个链表而不仅仅是一个,其中一个包含空闲块,另一个包含分块。当要分配新块时,我将简单地获取第一个提到的链表中的第一个元素并将其移动到分配的链表。据我所知,这将消除分配时进行任何搜索的需要,并且只留下需要搜索才能找到正确的块的解除分配。
任何想法都会受到赞赏,因为这是我第一次以这种方式直接使用内存。谢谢!
答案 0 :(得分:0)
使用std::list
(特别是如果将其与自定义分配器一起使用)可能更有效,而不是使用手工制作的链表。不易出错,可能更好地优化。
使用两个列表可以简化很多操作。无需在列表本身中跟踪块是否空闲 - 因为这将由块所在的列表指定(所需的只是确保块以某种方式不会出现在两个列表中)。
您当前的实现意味着您必须在分配和解除分配时使用链接列表。
如果块是固定大小的,那么只需通过将第一个可用块从空闲列表移动到分配列表来实现分配 - 无需搜索。要释放一个块,你仍然需要在分配的列表中找到它,这意味着你需要将T*
映射到列表中的一个条目(例如执行搜索),然后释放的行为将只需将条目从一个列表移动到另一个列表即可。
如果块是可变大小的,那么你需要做更多的工作。分配将需要在分配时找到至少为所请求大小的块。分配(分配比需要的更大的块)将使分配和释放在性能方面更有效,但也意味着可以从池中分配更少的块。或者,将一个大块(从空闲列表中)分成两部分,并在两个列表上放置一个条目(表示已分配的部分,而未分配的部分)。如果这样做,在解除分配时,可能需要合并内存中相邻的块(实际上,实现池中可用内存的碎片整理)。
您需要决定是否可以从多个线程使用池,并使用适当的同步。
答案 1 :(得分:0)
使用固定数量的大小分档,并使每个分区成为链接列表。
例如,假设你的垃圾箱只是系统页面大小的整数倍(通常是4KiB),你使用1MiB块;然后你有1MiB / 4KiB = 256个箱子。如果free在块中提供n页区域,则将其附加到bin n。分配n页区域时,从n到256遍历区域并选择第一个可用区块。
为了最大化性能,将bin与位图相关联,然后从位n-1扫描到位255以找到第一个设置位(使用编译器内在函数计算前导或尾随零,如__builtin_clz和_BitScanForward)。由于垃圾箱的数量,这仍然不是O(1),但它非常接近。
如果您担心内存开销,则可以为每个bin仅添加一次的每个块。也就是说,即使一个块有128个1页区域可用(最大碎片),bin 1仍然只会链接到块一次并重复使用128次。
要做到这一点,你必须在每个块内部将这些区域链接在一起,这意味着每个块还需要存储一个大小的列表 - 但这可以提高内存效率,因为最多只有256个每个块内的有效偏移量,而列表需要存储完整的指针。
请注意,无论哪种方式,如果您不希望每个块中的可用空间被分割,您都需要一种快速方法从列表中的bin中删除块 - 这意味着使用双向链表。显然,这会增加额外的内存开销,但在整个列表上进行定期的可用空间碎片整理仍然是可取的。