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

suggestions for improving an allocator algorithm implementation

本文关键字:实现 算法 分配器      更新时间:2023-10-16

我有一个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;

我需要能够为每个分配大约 32MB 的总大小。

分配

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

有一个std::vector< SuperBlock >,以防一个超级块没有足够的空间。

用于分配器的算法如下所示:

  1. 对于每个超级块:超级块的末尾有空间吗?把分配放在那里。(快速)
  2. 如果没有,请在每个超级块中搜索足够大小的空白空间,并将分配放在那里。(慢)
  3. 还是没有?分配另一个超级区块,并将分配放在新超级区块的开头。

不幸的是,步骤 2 在一段时间后可能会变得非常慢。随着对象副本的制作和临时变量的销毁,我得到了很多碎片。这会导致内存结构中的大量深度搜索。碎片是有问题的,因为我可以使用的内存量有限(请参阅下面的注释)

任何人都可以建议改进此算法以加快该过程吗?我是否需要两个单独的算法(一个用于固定大小分配,一个用于字符串分配器)?

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

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

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

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

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

基于Tim的回答,我个人会使用类似于BiBOP的东西。

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

对此有一些改进。

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

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

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

二、如何处理水池?使用链表。

这些页面通常是非类型的(本身),因此您有一个免费的页面列表,可以在其中准备新页面并放置"回收"页面。

对于每个大小类别,您都有一个"占用"页面的列表,其中已分配内存。对于您保留的每个页面:

  • 分配大小(对于此页面)
  • 已分配对象的数量(用于检查空性)
  • 指向第一个空闲单元格的指针
  • 指向上一页和下一页的指针(可能指向列表的"标题")
每个自由单元

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

给定大小的"占用"页面列表只需管理:

  • 删除时:如果清空页面,则将其从列表中删除并推送到回收页面中,否则,更新此页面的自由单元格列表(注意:查找当前页面的开头通常是对地址的简单取模运算)
  • 插入
  • 时:从头部开始搜索,一旦找到非完整页面,将其移动到列表前面(如果尚未)并插入您的项目

此方案在内存方面确实具有高性能,只保留了一个页面用于索引。

对于多线程

/多进程应用程序,您需要添加同步(通常每页互斥锁),以防您可以从Google的tcmalloc中获得灵感(尝试找到另一个页面而不是阻止,使用线程本地缓存来记住您上次使用的页面)。

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

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

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

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

如果某个项目适合上次分配的项目,只需分配它即可。 如果它命中固定项目,请将下一个分配标记移到固定项目之外,找到下一个较高的固定项目,然后再次尝试分配。 如果项目与项目列表冲突,但某处有足够的可用空间,请压缩块的内容(如果固定了一个或多个项目,如果有一个超级块可用,最好使用另一个超级块)。 根据使用模式,可能需要从仅压缩自上次收集以来添加的内容开始;如果这不能提供足够的空间,那么压缩所有内容。

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

我同意 Tim 的观点 - 使用内存池来避免碎片。

但是,您可以通过在矢量中存储指针而不是对象来避免一些改动,也许ptr_vector?