【问题标题】:How can I improve garbage collector performance of .NET 4.0 in highly concurrent code?如何在高并发代码中提高 .NET 4.0 的垃圾收集器性能?
【发布时间】:2011-01-19 15:34:53
【问题描述】:

我正在使用 .NET 框架 4 中的任务并行库(特别是 Parallel.ForParallel.ForEach),但是在并行化一些看起来应该可以轻松并行化的任务时,我的速度提升非常平庸。核心机。

在分析系统时,由于垃圾收集器的原因,看起来有很多线程同步正在进行。我正在做大量的对象分配,所以我想知道如何在最大限度地减少代码重写的同时提高并发性。

例如,在这种情况下是否有一些有用的技术:

  • 我应该尝试手动管理 GC 吗?
  • 我应该使用Dispose 吗?
  • 我应该固定对象吗?
  • 我应该做其他不安全的代码技巧吗?

后记:

问题不在于 GC 运行太频繁,而在于 GC 阻止了并发代码高效地并行运行。我也不认为“分配更少的对象”是一个可以接受的答案。这需要重写太多的代码来解决并行化不佳的垃圾收集器。

我已经找到了一个有助于提高整体性能的技巧 (using gcServer),但它对并发性能没有帮助。换句话说,在令人尴尬的并行任务中,Parallel.For 仅比串行 For 循环快 20%。

后置脚本:

好的,让我进一步解释一下,我有一个相当大而复杂的程序:优化解释器。它足够快,但我希望它在给定并行任务(内置于我的语言中的原始操作)时的性能能够在更多内核可用时很好地扩展。我在评估期间分配了很多小对象。整个解释器设计基于从单个多态基础对象派生的所有值。这在单线程应用程序中效果很好,但是当我们尝试将任务并行库应用于并行评估时,就没有优势了。

在对为什么任务并行库没有为这些任务正确地跨内核分配工作进行大量调查之后,罪魁祸首似乎是 GC。显然 GC 似乎充当了瓶颈,因为它在后台执行了一些我不明白的线程同步。

我需要知道的是:GC 到底在做什么会导致大量并发代码在执行大量分配时表现不佳,以及我们如何解决这个问题除了只是分配更少的对象。我已经想到了这种方法,并且需要大量重写大量代码。

【问题讨论】:

  • 是否可以发布实际代码,或者至少是它的代表?我认为我们需要深入研究细节以帮助您...
  • 代码将完全无法理解。这是优化解释器的一个非常深的部分。
  • “非常平庸”?那可能吗? ;)
  • 任务有多“小”,它们需要多少协调/通信/全局数据?尽管有任何 GC“开销”,但所有这些都很容易导致非并行有用的循环。祝你好运!
  • 一个想法,可能完全不相关:您尝试过发布版本还是仅使用调试版本进行测量?

标签: .net performance concurrency garbage-collection parallel-processing


【解决方案1】:

GC 究竟做了什么会导致大量并发代码在执行大量分配时表现不佳

.NET GC 可能正在序列化您分配的对象的复制和收集。 .NET GC 是一个标准的分代收集器,它将 Nursery (gen0) 拆分为单独的区域以用于单独的内核/线程,以便处理一些并行性。但是从所有内核分配的所有数据的收集显然是串行完成的。

但是,在这种情况下,我不相信 GC 是您的问题的根源。有很多方法可以在多核上实现较差的可扩展性。未能利用缓存是另一种常见的情况,它最终导致所有内核停止访问共享内存,以几乎无法察觉的方式扼杀可扩展性......

【讨论】:

    【解决方案2】:

    我有一个想法——为什么不尝试另一种 GC 实现呢? .NET 提供了三个。

    http://blogs.msdn.com/maoni/archive/2004/09/25/234273.aspx

    根据您的问题描述,我很想知道服务器 GC 是如何为您工作的,因为它为每个核心提供了一个单独的堆。 .NET 4 添加的后台 GC 模式可能也值得研究。

    http://blogs.msdn.com/maoni/archive/2008/11/19/so-what-s-new-in-the-clr-4-0-gc.aspx

    希望这对您的具体案例比目前的答案更有帮助。

    【讨论】:

      【解决方案3】:

      并行任务甚至原始线程都不是让您的代码运行得更快的灵丹妙药。如果你有任何锁、资源或只有几个内核,你可以减慢我尝试多线程的代码。您还需要确保您没有进行上下文交换,并且希望您拥有超过 4 个内核。 (不要忘记 GC、CLR、Windows 以及其他应用程序和服务都在争夺资源/周期。)

      您还应该知道,固定和不安全的代码可能会减慢某些操作。它们需要来自 CLR 和 GC 的特殊操作,以确保内存和资源的安全(例如,如果您 pin 或如果您 unsafe,则 GC 也无法压缩。)

      已为一般用途创建了并行任务库。如果您需要高度优化的代码,您可能还需要管理自己的线程。 (不像很多博客说的……这个行业没有灵丹妙药。)

      最好的办法是为每个线程创建一个工作类实例,以避免每个操作的构造和解构。查看ThreadStaticAttribute。据我了解,.Net 4.0 中还有其他选项,但我还没有机会使用它们。

      【讨论】:

        【解决方案4】:

        在分析系统时,由于垃圾收集器的原因,看起来有很多线程同步正在进行。我正在做很多对象分配,所以我想知道如何在最大限度地减少代码重写的同时提高并发性。

        不要做大量的对象分配。加快代码速度的唯一通用方法是减少工作量。如果GC耗时过长,理论上有两种选择:

        • 实施更好的 GC,或
        • 减少 GC 的工作量

        第一点几乎是不可能的。首先要替换 .NET GC 需要大量的黑客攻击,并且设计一个与 .NET 一样高效的 GC 也需要大量的工作。

        第二点确实是您唯一的选择:如果垃圾回收需要同步,请确保发生的回收更少。它们通常发生在 gen0 堆太满而无法满足分配请求时。

        所以请确保不会发生这种情况。不要分配这么多的对象。你有几种方法可以避免它:

        1. 使用(堆栈分配的)结构而不是类可能有助于降低 GC 压力。特别是小的、短命的对象可能会从转换为结构中受益,
        2. 重用您分配的对象。寿命较长的对象被移动到很少发生收集的较大堆中。例如,将分配移出循环。

        【讨论】:

          【解决方案5】:

          为了你的四点:

          1. How can I improve garbage collector performance of .NET 4.0 in highly concurrent code? (1)
          2. 如果您的对象拥有资源,尤其是非托管对象的资源,您应该处置。 Dispose 立即执行。可能的终结器(C++ 中的~析构函数)仅在 GC 运行并且对象从内存中删除时才会被调用。
          3. 仅当对象被传递给非托管代码片段时,固定对象才有意义,例如一个非托管的 c++ dll。否则,让垃圾收集器在保持内存整洁方面尽其所能。固定也可能导致内存碎片。
          4. 如果您不必这样做,则不需要。

          要考虑的一件事是将分配移出循环 - 如果可能的话。在许多情况下,当您可以这样做时,它还允许您重用已分配的对象,从而提供额外的性能(至少我的经验表明)(另请参阅 How can I improve garbage collector performance of .NET 4.0 in highly concurrent code?)。

          并行执行的等级始终取决于您正在执行的任务,在计算的情况下,最大可实现的并行度为

          【讨论】:

          • " 在计算的情况下,最大可实现的并行度为
          【解决方案6】:

          1) 您不能也不应该手动管理 GC。

          2) Dispose 只是对 GC 的一个指示,只要他觉得合适,它就会通过。 :P

          避免这些问题的唯一方法是分析您的应用并尽可能避免分配新对象。 当您找到垃圾收集器中的内容后,请尝试一些池化技术来重用这些数据并避免每次都重新创建它。

          编辑: 每当 GC 运行时,所有线程都必须进入睡眠状态以允许它完成工作。如果收藏品与您的情况一样多,这就是放缓的原因。除了减少新对象的生成之外,没有其他方法可以管理此问题。

          【讨论】:

          • Dispose,释放与对象关联的资源。垃圾收集器从内存中删除对象。当垃圾收集器决定是时候删除对象时,Disposed 会在被调用时运行,这是一个可能的终结器(在对象销毁之前调用)。
          • 您的解决方案似乎实际上是“不要分配这么多对象”,对吗?你能说服我为什么这是最好的选择,或者提供更多信息吗?例如,为什么垃圾收集器在处理大量对象和高度并发的代码时如此糟糕?如果您可以扩展您的答案,我会投票。
          • 添加了更多信息,但实际上没有其他方法。 :P
          • @cdiggins:也许你想实现一个低开销的并发垃圾收集器? ;) 它是如此“蹩脚”,因为你要求它做不可能的事情。您分配的对象越多,GC 运行的频率就越高。虽然是的,存在并发 GC,但它们的效率通常要低得多。 .NET 的目标是高效的 GC,但代价是失去了并发运行它的能力。这真的是常识:如果 GC 花费的时间太多,那就少做点工作。
          • “.NET 的目标是实现高效的 GC,但代价是失去了并发运行它的能力”。多核上的默认工作站 GC所谓的多并发 GC。
          【解决方案7】:

          这是生活中的事实。几乎所有的内存管理方案都会对在某种程度上看起来令人尴尬的并行代码进行序列化。我认为 C# 有线程本地分配器,所以它应该只对集合进行序列化。尽管如此,我还是建议汇集/重用您最常分配的对象和数组,并可能将一些小的非多态对象转换为结构,看看是否有帮助。

          【讨论】:

            【解决方案8】:

            如果由于分配/GC-ed 的对象过多而导致 GC 运行过于频繁,请尝试分配更少的对象:)

            根据您的情况 - 尝试重用现有对象、创建对象池、使用不会造成太大内存压力的“较轻”对象(或更大以减少分配的对象数量)。

            不要试图通过显式调用 GC.Collect 来“管理 GC”,它很少有回报 (Rico Mariani says so)

            http://blogs.msdn.com/ricom/archive/2003/12/02/40780.aspx

            【讨论】:

            • 问题不是GC运行太频繁,而是GC阻碍了并发代码高效并行运行。
            • 我不知道你的具体场景,但是你是否怀疑如果GC每10秒而不是每10毫秒运行一次,它仍然会阻止并发代码有效地并行运行吗?跨度>
            • 我不认识 Marek,我完全无法理解 GC 是如何明显触发如此多的同步事件,并阻止我的应用程序有效利用多个内核的。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2013-04-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-02-27
            • 1970-01-01
            相关资源
            最近更新 更多