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

12
我正在使用来自.NET Framework 4的任务并行库(具体来说是Parallel.ForParallel.ForEach),然而在一些看起来应该可以轻松并行化的任务上,并行效果却非常平庸,尤其是在双核机器上。在对系统进行分析时,发现由于垃圾回收器存在很多线程同步问题。由于我正在大量分配对象,因此我想知道如何在最小改写代码的情况下提高并发性。
例如,在这种情况下是否有一些有用的技术:
  • 我应该尝试手动管理GC吗?
  • 我应该使用Dispose吗?
  • 我应该固定对象吗?
  • 我应该使用其他不安全的代码技巧吗?
后记:
问题不在于GC运行太频繁,而在于GC防止并发代码有效地并行运行。我也不认为“分配较少的对象”是一个可接受的答案。这要求改写过多的代码以解决垃圾回收器无法良好并行化的问题。
我已经找到了一个技巧,从总体上提高了性能(使用gcServer),但它并没有提高并发性能。换句话说,在一个明显可以并行处理的任务上,Parallel.For只比串行For循环快20%。
后后记:

好的,让我进一步解释一下。我有一个相当大且复杂的程序:一个优化解释器。它已经足够快了,但是当给定并行任务(内置于我的语言中的原始操作)时,我希望它的性能随着更多的核心而扩展得更好。在评估过程中,我分配了许多小对象。整个解释器设计基于所有值都来自单个多态基对象。这在单线程应用程序中非常有效,但是当我们尝试将任务并行库应用于并行评估时,就没有优势。

经过大量调查,为什么任务并行库无法正确地在这些任务之间分配工作,看来罪魁祸首是GC。显然,GC似乎成为瓶颈,因为它执行了一些我不理解的后台线程同步。

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


能否发布实际的代码,或者至少是它的表示形式?我认为我们需要深入具体细节来帮助你。 - Rob Fonseca-Ensor
代码将会完全无法理解。这是优化解释器的一个非常深入的部分。 - cdiggins
5
“极其平庸”?这可能吗? ;) - jalf
这些任务有多“小”,需要多少协调/沟通/全局数据?尽管存在任何GC的“开销”,所有这些都很容易导致非并行有用的循环。祝好运! - sdg
一个想法,也许完全无关:你尝试过使用发布版本进行测试吗?还是只测量了调试版本? - Marek
8个回答

5
如果由于分配/回收太多对象而导致GC运行太频繁,请尝试减少它们的分配。根据你的情况,尝试重用现有对象、创建对象池、使用“轻量级”对象来减少内存压力(或者增加对象大小以减少分配数量)。不要通过显式调用GC.Collect来“管理GC”,这很少有回报。具体请参考Rico Mariani的文章:http://blogs.msdn.com/ricom/archive/2004/11/29/271829.aspxhttp://blogs.msdn.com/ricom/archive/2003/12/02/40780.aspx

问题不在于GC运行太频繁,而是GC阻止并发代码高效地并行运行。 - cdiggins
我不知道你的具体情况,但是你是否怀疑如果垃圾回收器每10秒运行一次而不是每10毫秒运行一次,它是否仍然能够有效地防止并发代码的并行运行? - Marek
我不认识Marek,但我很困惑为什么垃圾回收器会触发如此多的同步事件,从而阻止我的应用程序有效地利用多个核心。 - cdiggins

2

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

2)Dispose仅是向GC发出的指示,它会在自己认为合适的时候进行垃圾回收。:P

避免这些问题的唯一方法是对您的应用程序进行分析,并尽可能避免分配新对象。当您找到正在进入垃圾回收器的对象后,请尝试一些池技术来重复使用这些数据,并避免每次重新创建它。

编辑: 无论何时GC运行,所有线程都必须进入睡眠状态,以允许其完成工作。如果集合很多(如您的情况),那么这就是减速的原因。管理此问题的唯一可能方法是减少新对象的生成。


1
Dispose释放与对象相关联的资源。垃圾收集器从内存中删除该对象。当调用Disposed时,可能会运行一个终结器(在垃圾收集器决定删除对象之前调用)。 - AxelEckenberger
你的解决方案似乎是“不要分配太多对象”,对吗?你能说服我这是最好的选择,或者提供更多信息吗?例如,为什么垃圾回收器在处理许多对象和高并发代码时如此低效呢?如果你能扩展你的答案,我会点赞的。 - cdiggins
添加了更多信息,但实际上没有其他方式。 :P - feal87
@cdiggins:也许你想实现一个低开销的并发垃圾收集器? ;) 这是如此“可恶”,因为你要求它做不可能的事情。你分配的对象越多,GC 就必须运行得越频繁。虽然有并发 GC 存在,但它们通常效率要低得多。.NET 旨在实现高效的 GC,代价是失去了并发运行的能力。这真是常识:如果 GC 花费太多时间,请给它少一些工作量。 - jalf
“ .NET 旨在实现高效 GC ,但代价是失去了并发运行的能力”。在多核计算机上,默认工作站 GC 被称为几乎并发的 GC。 - J D
这篇文章是正确的。基本上和我对类似问题的回答一样:要么让垃圾回收器自己处理,使用池化技术,或者干脆不使用托管框架——而且,没错,当垃圾回收器运行时所有线程都会被锁定。我不确定为什么这么难理解。至少他们没有降低你的评分,feal87... - Jin

2
针对您的四个问题:
  1. 请参考以下链接:如何在高并发代码中提高.NET 4.0垃圾回收器的性能?(1)
  2. 如果您的对象持有资源,尤其是非托管对象的资源,应该进行处理。Dispose会立即执行,而可能的终结器(类似于C++中的析构函数)只有在GC运行并将对象从内存中移除时才会调用。
  3. 只有在将对象传递给非托管代码(例如非托管的C++ DLL)时,钉住对象才有意义。否则,请让垃圾回收器负责保持内存整洁。固定还可能导致内存碎片化。
  4. 如果没有必要,就不要这样做。

还有一件事需要考虑,那就是将分配从循环中移出——如果可能的话。在许多情况下,这样做还允许您重用已经分配的对象,从而提供额外的性能(至少这是我的经验),详见如何在高并发代码中提高.NET 4.0垃圾回收器的性能?

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


在计算过程中,最大可实现的并行性为<n倍,其中n是处理器数量-纯计算。在输入或输出操作的情况下,通常会超过n。这里的IO指的是读写内存。 - J D

2

1

这是生活的一个事实。几乎所有的内存管理方案都会以某种程度的尴尬并行来串行化代码。我认为C#有线程本地分配器,因此它只应在集合上进行串行化。尽管如此,我建议您池化/重复使用最常分配的对象和数组,或者将一些小型的、非多态的对象转换为结构体,并查看是否有帮助。


1
在对系统进行分析时,看起来由于垃圾收集器的原因,有很多线程同步正在进行。我正在大量分配对象,所以我想知道如何在最小化代码重写的同时改进并发性能。
不要频繁地分配对象。加快代码速度的唯一通用方法是让它做更少的工作。如果垃圾收集需要太长时间,有两个理论选项:
- 实现更好的垃圾收集器;或 - 减少垃圾收集器的负载
第一个点几乎是不可能的。首先,替换.NET GC需要大量技术性的操作,其次设计一个与.NET垃圾回收器相当高效的GC也需要大量的工作。
第二个点是你真正的选择:如果垃圾收集需要同步,请确保减少垃圾回收的次数。一般情况下,当gen0堆的空间填满无法满足内存分配请求时,垃圾回收就会发生。
因此,请确保这种情况不会发生。不要分配过多的对象,你有几种避免这种情况的方法:
1. 使用(堆栈分配的)结构体而不是类可能有助于减少GC压力。尤其是小型,短期存在的对象可能会从转换为结构体中受益。 2. 重复使用您分配的对象。长期存在的对象被移动到较大的堆中,很少进行集合。例如,将分配移出循环。

1
.NET GC 可能会对您分配的对象进行复制和收集的序列化操作,这可能导致高并发代码性能下降。.NET GC 是标准的分代收集器,将幼年代(gen0)分成不同的区域以处理一些并行性,但是从所有核心分配的数据的收集显然是串行完成的。
然而,在这种情况下,我并不认为 GC 是您问题的根源。在多核上实现差劲的可扩展性有很多方法。未能利用缓存是另一个常见的问题,它会导致所有核心在访问共享内存时停滞,以一种几乎无法检测到的方式破坏可扩展性...

0

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

此外,你还应该知道固定内存和不安全代码可能会减慢一些操作。它们需要CLR和GC进行特殊的操作,以确保内存和资源的安全(例如,如果你“固定”或者使用“不安全”的话,GC就无法很好地压缩内存)。

并行任务库是为通用目的而创建的。如果你需要高度优化的代码,你可能也需要自己管理自己的线程。(与许多博客说的不同,在这个行业中没有什么灵丹妙药。)

你最好的选择是为每个线程创建一个工作类的实例,以避免每个操作的构建和拆除。请查看ThreadStaticAttribute。据我所知,在 .Net 4.0 中还有其他选项,但我还没有机会使用它们。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接