【问题标题】:Heap optimized for (but not limited to) single-threaded usage堆优化(但不限于)单线程使用
【发布时间】:2012-05-28 03:27:31
【问题描述】:

我在我的一个项目中使用了自定义堆实现。它由两个主要部分组成:

  1. 固定大小的块堆。 IE。仅分配特定大小的块的堆。它分配更大的内存块(虚拟内存页或来自另一个堆),然后将它们划分为原子分配单元。

    它执行分配/释放速度很快(在 O(1) 中)并且没有内存使用开销,不考虑外部堆施加的东西。

  2. 全局通用堆。它由上述(固定大小)堆的桶组成。 WRT 请求的分配大小,它选择适当的存储桶,并通过它执行分配。

    由于整个应用程序(大量)是多线程的 - 全局堆在其操作期间锁定适当的存储桶。

    注意:与传统堆相比,这种堆不仅需要分配大小,还需要释放。这允许在没有搜索或额外内存开销(例如保存分配块之前的块大小)的情况下识别适当的存储桶。虽然不太方便,但在我的情况下这是可以的。此外,由于“桶配置”在编译时是已知的(通过 C++ 模板 voodoo 实现) - 适当的桶是在编译时确定的。

到目前为止,一切看起来(并且工作)都很好。

最近我研究了一种算法,该算法会大量执行堆操作,并且自然会受到堆性能的显着影响。分析显示其性能受到锁定的显着影响。也就是说,堆本身的工作速度非常快(典型的分配只涉及一些内存取消引用指令),但由于整个应用程序是多线程的 - 适当的存储桶受关键部分保护,它依赖于 interlocked 指令,要重得多。

我同时通过为该算法提供自己的专用堆来解决此问题,该堆不受关键部分的保护。但这在代码级别施加了几个问题/限制。例如需要在堆栈深处传递上下文信息的地方可能需要堆。也可以使用 TLS 来避免这种情况,但在我的具体情况下,这可能会导致重新进入时出现一些问题。

这让我想知道:是否有一种已知的技术可以优化堆以用于(但不限于)单线程使用?

编辑:

特别感谢 @Voo 建议检查 google 的 tcmalloc。

它似乎或多或少地与我所做的相似(至少对于小物体)。但此外,他们通过维护每个线程缓存解决了我遇到的确切问题。

我也想过这个方向,但我考虑过维护每个线程的。然后释放从属于另一个线程的堆中分配的内存块有点棘手:应该将它插入某种锁定队列中,并且应该通知另一个线程,并异步释放挂起的分配。异步释放可能会导致问题:如果该线程由于某种原因很忙(例如执行激进的计算) - 实际上不会发生内存释放。此外,在多线程场景中,重新分配的成本要高得多。

OTOH 缓存的想法似乎更简单,更有效。我会努力解决的。

非常感谢。

附注:

确实 google 的 tcmalloc 很棒。我相信它的实现与我所做的非常相似(至少是固定大小的部分)。

但是,为了迂腐,有一个问题是我的堆更胜一筹。根据文档,tcmalloc 会产生大约 1% 的开销(渐近),而我的开销是 0.0061%。准确地说是 4/64K。

:)

【问题讨论】:

  • 这让我想起了我多年前所做的测试。常用的“差”标准机制占用了好的“自定义”实现的 100 倍以上。
  • 如果比标准内存分配器更快,我很想看看你做了什么。由于大多数标准实现已经完成了您声称要做的事情(以及更多)。我发现 O(1) 声明很奇怪,尤其是当您声明没有开销时(我相信当您的专利通过时,您会赚到一大笔钱)。
  • 整个存储桶的想法基本上是 google 的 tcmalloc(尽管这是一个通用分配器,它必须动态决定使用哪个存储桶)。 tcmalloc 确实使用线程本地存储来完全避免您的问题,并且很少从一般堆中分配,因此避免了锁。

标签: c++ c heap-memory


【解决方案1】:

一种想法是为每个线程维护一个内存分配器。从全局内存池中为每个分配器预先分配相当大的内存块。设计你的算法来分配来自相邻内存地址的大块(稍后会详细介绍)。

当给定线程的分配器内存不足时,它会从全局内存池中请求更多内存。此操作需要锁定,但发生的频率应该远低于您当前的情况。当给定线程的分配器释放它的最后一个字节时,将该分配器的所有内存返回到全局内存池(假设线程已终止)。

这种方法会比您当前的方法更早地耗尽内存(内存可以保留给一个永远不需要它的线程)。这是一个问题的程度取决于您的应用程序的线程创建/生命周期/销毁配置文件。您可以以增加复杂性为代价来缓解这种情况,例如通过引入一个信号,表明给定线程的内存分配器内存不足,并且全局池已用尽,其他内存分配器可以通过释放一些内存来响应。

这种方案的一个优点是它倾向于消除false sharing,因为给定线程的内存将倾向于分配在连续的地址空间中。

顺便说一句,如果您还没有阅读它,我建议任何实现自己的内存管理的人使用IBM's Inside Memory Management 文章。

更新

如果目标是为多线程环境优化非常快速的内存分配(而不是自己学习如何去做),请查看备用内存分配器。如果目标是学习,不妨看看他们的源代码。

【讨论】:

  • 除了Hoarde还有tcmalloc,它基本上是OPs提出的方案,具有明显的(线程本地堆)和不太明显的改进。当我测试它时,它比 Hoarde 更快,但我认为这取决于用例。
  • @Voo:确切地说,它不是线程本地的heap,而是线程本地的cache,这是一种完全不同的动物。请阅读我更新的问题。附言我希望你也对我的堆进行基准测试,看看它的比较情况。
  • 感谢您的建议。我喜欢倾向于为每个线程分配连续的内存块以降低错误共享的机会。但是恕我直言,真正的“最佳位置”是每个线程的缓存,而不是每个线程的allocator。它非常简单,并且可能为单线程使用产生最佳性能,而不会显着降低多线程性能。是的,虚假分享的可能性更高,但在典型情况下这将是次要的恕我直言。
  • 很好奇...为什么您认为虚假分享不是主要问题?我个人没有在广泛的应用程序中对其进行测量,但可能造成虚假分享的场景似乎很常见。
  • @Eric J:我不是说它不会发生,它自然会发生。但这实际上几乎与堆策略无关。甚至可以在同一个线程中分配相邻的内存块,但在不同的线程中使用它们。我说这似乎是一个小问题,因为在最坏的情况下您会丢弃 CPU 缓存内容,而锁定意味着 保证 避免缓存 + 内存屏障,这更昂贵。我相信对于随机分配和访问的数据,虚假共享应该是次要的。否则这将意味着缓存行的整个想法是错误的。
【解决方案2】:

阅读 Jeff Bonwicks 关于平板分配器和 vmem 的经典论文可能是个好主意。原始的平板分配器听起来有点像你在做什么。虽然对多线程不太友好,但它可能会给您一些想法。

The Slab Allocator: An Object-Caching Kernel Memory Allocator

然后他用 VMEM 扩展了这个概念,这肯定会给你一些想法,因为它在多 cpu 环境中具有非常好的行为。

Magazines and Vmem: Extending the Slab Allocator to Many CPUs and Arbitrary Resources

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多