改进分配器算法实现的建议

时间:2011-05-17 16:39:26

标签: c++ algorithm allocator

我有一个Visual Studio 2008 C ++应用程序,我正在为标准容器使用自定义分配器,以便它们的内存来自内存映射文件而不是堆。此分配器用于4种不同的用例:

  1. 104字节固定大小结构std::vector< SomeType, MyAllocator< SomeType > > foo;
  2. 200字节固定大小结构
  3. 304字节固定大小结构
  4. n字节字符串std::basic_string< char, std::char_traits< char >, MyAllocator< char > > strn;
  5. 我需要为这些中的每一个分配大约32MB的总和。

    分配器使用std::map指向分配大小的指针来跟踪内存使用情况。 typedef std::map< void*, size_t > SuperBlock;每个SuperBlock代表4MB内存。

    如果一个SuperBlock空间不足,则会有std::vector< SuperBlock >个。

    用于分配器的算法如下:

    1. 对于每个SuperBlock:超级块的末尾是否有空格?把分配放在那里。 (快)
    2. 如果没有,请在每个SuperBlock中搜索足够大小的空白空间并将分配放在那里。 (慢)
    3. 还是什么都没有?分配另一个SuperBlock并将分配放在新的SuperBlock的开头。
    4. 不幸的是,一段时间后,第2步可能变得非常缓慢。随着对象的复制和临时变量的破坏,我得到了很多碎片。这导致在存储器结构内进行大量深度搜索。碎片存在问题,因为我使用的内存有限(参见下面的注释)

      有人可以建议对此算法进行改进,以加快这一过程吗?我需要两个单独的算法(1个用于固定大小的分配,1个用于字符串分配器)?

      注意:对于那些需要理由的人:我在Windows Mobile中使用此算法,其中Heap有32MB的进程槽限制。所以,通常的std::allocator不会削减它。我需要将分配放在1GB大内存区域中以获得足够的空间,这就是它的作用。

6 个答案:

答案 0 :(得分:6)

您是否可以为要分配的每种不同的固定大小类型设置单独的内存分配池?这样就不会有任何碎片,因为分配的对象将始终在n字节边界上对齐。当然,这对可变长度字符串没有帮助。

在Alexandrescu的 Modern C++ design 中有一个小对象分配的例子,它说明了这个原则,可能会给你一些想法。

答案 1 :(得分:2)

对于固定大小的对象,您可以创建固定大小的分配器。基本上,您可以分配块,将分区划分为适当大小的子块,并使用结果创建链接列表。如果有可用内存(从列表中删除第一个元素并返回指向它的指针),则从这样的块分配是O(1),因为是解除分配(将块添加到空闲列表)。在分配期间,如果列表为空,则获取新的超级块,分区并将所有块添加到列表中。

对于可变大小的列表,您可以通过仅分配已知大小的块来将其简化为固定大小的块:32字节,64字节,128字节,512字节。您将不得不分析内存使用情况以提出不同的存储桶,这样您就不会浪费太多内存。对于大型对象,您可以返回动态大小分配模式,这种模式会很慢,但希望大对象的数量有限。

答案 2 :(得分:2)

根据蒂姆的回答,我个人会使用类似于BiBOP的东西。

基本思路很简单:使用固定大小的池。

对此有一些改进。

首先,池的大小通常是固定的。这取决于您的分配例程,通常如果您在使用malloc时知道您正在处理的操作系统一次至少4KB,那么您使用该值。对于内存映射文件,您可以增加此值。

固定大小池的优势在于它可以很好地抵御碎片。所有页面大小相同,您可以轻松地将空256字节页面回收到128字节页面。

大型对象仍有一些碎片,通常在此系统之外分配。但它很低,特别是如果你将大型对象放入页面大小的倍数,这样内存就很容易回收。

其次,如何处理池?使用链接列表。

页面通常是无类型的(单独使用),因此您有一个免费的页面列表,可以在其中准备新页面并放置“回收”页面。

对于每个尺寸类别,您都有一个“已占用”页面列表,其中已分配了内存。对于您保留的每个页面:

  • 分配大小(针对此页面)
  • 分配的对象数量(检查空虚)
  • 指向第一个空闲单元格的指针
  • 指向上一页和下一页的指针(可能指向列表的“头部”)

每个自由单元本身就是指向下一个自由单元格的指针(或索引,具体取决于你所拥有的大小)。

管理给定大小的“已占用”页面列表:

  • 关于删除:如果您清空页面,然后将其从列表中删除并将其推入回收页面,否则,请更新此页面的空白单元格列表(注意:查找当前页面的开头通常是对地址进行简单的模运算)
  • 关于插入:从头部开始搜索,一旦找到非整页,请将其移到列表前面(如果还没有)并插入项目

这种方案在内存方面确实非常高效,只有一页保留用于索引。

对于多线程/多进程应用程序,您需要添加同步(通常每页一个互斥锁),以防您从Google的tcmalloc获取灵感(尝试找到另一个页面而不是阻止,使用一个线程-local cache来记住你上次使用的页面。

话虽如此,你试过Boost.Interprocess吗?它提供了分配器。

答案 3 :(得分:1)

对于固定大小,您可以轻松使用小内存分配器类型的分配器,您可以在其中分配一个大块,该块被拆分为固定大小的块。然后创建一个指向可用块的指针向量,并在分配/释放时弹出/推送。这非常快。

对于可变长度的项目,它更难:您要么必须处理搜索可用的连续空间或使用其他方法。您可以考虑维护按块大小排序的所有自由节点的另一个映射,这样您可以降低地图的地图,如果下一个可用节点只说5%太大则返回它,而不是尝试找到确切大小的可用空间。

答案 4 :(得分:1)

如果可行的话,我对可变大小项目的倾向是避免直接指向数据,而是保留句柄。每个句柄都是超级块的索引,以及超级块中项目的索引。每个超级块都有一个自上而下分配的项目列表和自下而上分配的项目。每个项目的分配将以其长度和它所代表的项目的索引开头;使用索引的一位来指示某个项目是否为“固定”。

如果项目适合最后分配的项目,则只需分配它。如果它会击中固定项目,则将下一个分配标记移动到固定项目之后,找到下一个更高固定项目,然后再次尝试分配。如果项目将与项目列表发生冲突,但某处有足够的可用空间,则会对块的内容进行压缩(如果一个或多个项目被固定,则最好使用另一个超级块(如果有的话可用))。根据使用模式,可能需要首先仅对自上次收集后添加的内容进行压缩;如果这没有提供足够的空间,那么就会使一切变得紧凑。

当然,如果只有一些离散大小的项目,你可以使用简单的固定大小的块分配器。

答案 5 :(得分:1)

我同意蒂姆 - 使用内存池来避免碎片。

但是,您可以通过在向量中存储指针而不是对象来避免某些流失,也许ptr_vector