如何在高度并发的代码中提高.NET 4.0的垃圾收集器性能?

时间:2010-02-22 13:37:55

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

我正在使用.NET framework 4中的任务并行库(特别是Parallel.ForParallel.ForEach)但是当并行化一些看起来应该很容易并行化的任务时,我得到了极其平庸的加速在双核机器上。

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

例如,有些技术在这种情况下很有用:

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

后记:

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

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

POST-后记:

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

经过大量调查,为什么任务并行库没有为这些任务在核心之间正确分配工作,似乎罪魁祸首是GC。显然GC似乎是一个瓶颈,因为它在我不理解的场景线程同步背后做了一些。

我需要知道的是:GC究竟做了什么会导致大量并发代码在执行大量分配时执行得很糟糕,以及我们如何解决 < i>分配更少的对象。我已经想到了这种方法,并且需要对大量代码进行重大改写。

8 个答案:

答案 0 :(得分:5)

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

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

不要通过明确调用GC.Collect来尝试“管理GC”,它很少得到回报(Rico Mariani says so

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

答案 1 :(得分:2)

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

2)处理只是GC的一个指示,只要他感觉正确就会通过。 :P

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

编辑: 每当GC运行时,所有线程都必须处于睡眠状态以允许它完成其工作。如果收集很多就像你的情况那样,这就是放缓的原因。除了减少新对象的生成之外,没有其他可能的方法来管理它。

答案 2 :(得分:2)

对于你的四点:

  1. How can I improve garbage collector performance of .NET 4.0 in highly concurrent code?(1)
  2. 如果对象拥有资源,尤其是非托管对象的资源,则应该处置。 Dispose立即执行。只有在GC运行并且从内存中删除对象时,才会调用可能的终结器(在C ++中为~detructor)。
  3. 只有将对象传递给非托管代码段,例如,才能确定对象的固定。一个非托管的c ++ DLL。其他,让垃圾收集器尽其所能保持内存整洁。固定也可能导致内存碎片化。
  4. 如果您不需要,则不是。
  5. 要考虑的一件事是将分配从循环中移出 - 如果可能的话。在许多情况下,当您可以执行此操作时,它还允许您重用已分配的对象,从而提供额外的性能(至少根据我的经验显示)(另请参阅How can I improve garbage collector performance of .NET 4.0 in highly concurrent code?)。

    并行执行的等级总是取决于您正在执行的任务,在计算的情况下,最大可实现的并行度<1。 n次,其中n是处理器的数量 - 纯计算。在输入或输出操作的情况下,通常会超过n。

答案 3 :(得分: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

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

答案 4 :(得分:1)

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

答案 5 :(得分:1)

  

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

不要做很多对象的分配。加速代码的唯一通用方法是减少工作量。如果GC花费太多时间,则有两种理论选择:

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

第一点几乎是不可能的。首先要更换.NET GC需要大量的黑客攻击,而且设计一个甚至远远高于.NET的GC也需要做很多工作。

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

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

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

答案 6 :(得分:1)

  

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

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

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

答案 7 :(得分:0)

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

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

已创建并行任务库以用于通用目的。如果您需要高度优化的代码,您可能还需要管理自己的线程。 (不像许多博客说的那样......在这个职业中没有神奇的子弹。)

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