【问题标题】:Efficient heap-manager for heavy churn, tiny allocs?高效的堆管理器,用于处理大量流失、微小的分配?
【发布时间】:2010-09-18 16:33:41
【问题描述】:

我正在寻找一个堆管理器的想法来处理非常具体的情况:大量非常小的分配,每个分配范围从 12 到 64 字节。任何更大的东西,我都会传递给常规的堆管理器,所以只需要处理小块。只需要 4 字节对齐。

我主要担心的是

  1. 开销。常规的 libc 堆通常会将分配四舍五入为 16 字节的倍数,然后添加另一个 16 字节标头 - 这意味着 20 字节分配的开销超过 50%,这很糟糕。
  2. 性能

一个有用的方面是 Lua(它是这个堆的用户)会在调用 free() 时告诉你它正在释放的块的大小 - 这可能会启用某些优化。

我将发布我目前的方法,它工作正常,但如果可能的话,我想改进它。有什么想法吗?

【问题讨论】:

    标签: c performance memory-management lua heap-memory


    【解决方案1】:

    可以构建一个对大小相同的对象非常有效的堆管理器。您可以为所需的每种大小的对象创建其中一个堆,或者如果您不介意使用一点空间,则为 16 字节对象创建一个,为 32 字节对象创建一个,为 64 个字节创建一个。最大开销为31 字节分配 33 字节(将在 64 块大小的堆上进行)。

    【讨论】:

    • 是的。我可能会低至 4 字节的粒度,但这听起来像是我需要的。所以这可以用完全无标题的块来完成,对吧?
    • 是的,不需要标题。分配的块不需要头,空闲块只需要指向下一个空闲块的指针。
    • 甜蜜。大概 GCing 它来回收内存会让人头疼,但我会看看我是否可以简单地避免这种情况,或者尝试逐步这样做。
    【解决方案2】:

    答案可能取决于这些对象的生命周期模式。如果对象在您继续进行时全部实例化,然后一举全部删除,则创建一个非常简单的堆管理器可能有意义,该堆管理器通过简单地递增指针来分配内存。然后,当你完成后,吹走整个堆。

    Raymond Chen 发了一个interesting post 可能有助于激发您的灵感。 :)

    【讨论】:

      【解决方案3】:

      如果在进行下一轮分配之前分配、使用和释放了一堆内存,我建议尽可能使用最简单的分配器:

      typedef struct _allocator {
          void* buffer;
          int start;
          int max;
      } allocator;
      
      void init_allocator(size_t size, allocator* alloc) {
          alloc->buffer = malloc(size);
          alloc->start = 0;
          alloc->max = size;
      }
      
      void* allocator_malloc(allocator* alloc, size_t amount) {
          if (alloc->max - alloc->start < 0) return NULL;
          void* mem = alloc->buffer + alloc->start;
          alloc->start += bytes;
          return mem;
      }
      
      void allocator_free(allocator* alloc) {
          alloc->start = 0;
      }
      

      【讨论】:

      • 对c++来说合理,目标语言是c。
      • 不是那么难转换成 C 语言吗?
      • 对不起,我应该提到 C++ 对我很好;不幸的是,问题在于 Lua 大量搅动堆,以至于这种方法不实用。
      【解决方案4】:

      为了扩展 Greg Hewgill 所说的,实现超高效的固定大小堆的一种方法是:

      1. 将大缓冲区拆分为节点。节点大小必须至少为 sizeof(void*)。
      2. 使用每个空闲节点的第一个 sizeof(void*) 字节作为链接指针,将它们串在一起形成一个单链表(“空闲列表”)。分配的节点不需要链接指针,因此每个节点的开销为 0。
      3. 通过删除列表的头部并返回它进行分配(2 个加载,1 个存储)。
      4. 在列表头部插入即可释放(1 次加载,2 次存储)。

      显然,第 3 步还必须检查列表是否为空,如果是,则进行大量工作以获取新的大缓冲区(或失败)。

      正如 Greg D 和 hazzen 所说,更有效的是通过递增或递减指针(1 次加载,1 次存储)进行分配,而根本不提供释放单个节点的方法。

      编辑:在这两种情况下,free 都可以处理“我在常规堆管理器上传递的任何更大的东西”的复杂性,因为您可以在调用 free 时恢复大小。否则,您将查看一个标志(每个节点的开销可能为 4 个字节),或者在您使用的缓冲区的某种记录中查找。

      【讨论】:

      • 非常感谢,请接受荣誉+1(我没有合适的投票帐户)。
      • PS:我可以避免 free() 复杂性,因为 Lua 告诉我它正在释放的块大小。因此,我可以非常快速地查找它来自哪个堆并采取相应措施。
      • 是的,我在发布后也注意到了这一点,但是我遇到了连接问题,所以你打败了我:-(
      【解决方案5】:

      我喜欢一个人的回答。

      您也可以考虑将 buddy system 用于您的固定大小堆集。

      【讨论】:

        【解决方案6】:

        我主要使用 O(1) 小块内存管理器 (SBMM)。基本上它是这样工作的:

        1) 它从操作系统分配更大的超级块,并将开始+结束地址作为一个范围进行跟踪。 SuperBlock 的大小是可调整的,但 1MB 的大小已经相当不错了。

        2) 超级块被分成块(大小也可调节...4K-64K 好,具体取决于您的应用程序)。这些块中的每一个都处理特定大小的分配,并将块中的所有项目存储为单链表。当你分配一个 SuperBlock 时,你会创建一个 Free Blocks 的链表。

        3) 分配一个项目意味着 A) 检查是否有一个带有空闲项目的块处理该大小 - 如果没有,则从超级块分配一个新块。 B) 从块的空闲列表中删除该项目。

        4) 通过地址释放项目意味着 A) 找到包含地址的超级块 (*) B) 在超级块中找到块(减去超级块的起始地址并除以块大小) C) 将项目推回块的空闲项目列表中。

        正如我所说,这个 SBMM 非常快,因为它以 O(1) 的性能 (*) 运行。在我实现的版本中,我使用了一个 AtomicSList(类似于 Windows 中的 SLIST),因此它不仅是 O(1) 性能,而且在实现中还有 ThreadSafe 和 LockFree。如果您愿意,您实际上可以使用 Win32 SLIST 来实现该算法。

        有趣的是,从超级块中分配块或从块中分配项目的算法产生几乎相同的代码(它们都是 O(1) 的空闲列表分配)。

        (*) 超级块排列在一个范围图中,平均性能为 O(1)(但在 N 是超级块的数量的最坏情况下可能存在 O(Lg N))。范围图的宽度取决于大致了解您需要多少内存才能获得 O(1) 性能。如果你过冲,你会浪费一点内存,但仍然可以获得 O(1) 性能。如果您低于目标,您将接近 O(Lg N) 性能,但 N 用于 SuperBlock 计数 - 而不是项目计数。由于 SuperBlock 计数与 Item 计数相比非常低(在我的代码中大约是 20 个二进制数量级),因此它不像分配器的其余部分那么重要。

        【讨论】:

          猜你喜欢
          • 2016-02-05
          • 2021-03-20
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-11-15
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多