如何减少在GC中花费的时间

8
我正在创建一个桌面应用程序,其中有一个计算密集型操作,可能需要运行几秒钟。显然需要尽量缩短此操作的时间。该操作很容易并行化(单个子任务),每个子任务在单个线程上需要约50ms。在多个线程上,每个子任务需要4-5倍的时间,因为40-50%的时间花费在GC上,实际上完全取消了速度提升。
因此,我需要让GC工作更少。我的第一个想法是尝试找出哪种类型的对象被垃圾回收最多,但我意识到,尽管我经常进行内存分析,但我从未搜索过这样的模式。通常查看堆快照或堆快照之间的差异,但这些显示的是活动对象,而不是在这些快照之间创建和处理的对象。因此,这是我的第一个问题:找到创建和垃圾回收最多的类型的最简单方法是什么?我尝试查找方法调用计数,以查看是否调用了某个构造函数,但是创建数百万个对象的所有对象都只是小的结构体类型。如果我理解正确,即使装箱,这些也不应对GC产生影响?
该算法创建了数十万个单独的结果点对象。当然,这些对象不应该被垃圾回收,因为它们代表操作的输出。但这引出了我的第二个问题:GC所花费的时间主要是取决于总对象数量还是实际收集的对象数量?我应该尝试限制结果对象的数量,而使用更少但更大的结果对象吗?
编辑:我使用VS 2010并发可视化器找到了在GC中所花费的时间。此外,在并行代码中,大多数阻塞线程的部分都在等待GC。
编辑:我应该澄清一下,性能问题是因为执行在工作站GC上实际上是串行化的。例如,参见此帖子中描述的性能问题。

http://blogs.msdn.com/b/hshafi/archive/2010/06/17/case-study-parallelism-and-memory-usage-vs2010-tools-to-the-rescue.aspx

我无法解决垃圾回收器阻塞我的线程的问题(而且我认为对于桌面应用程序,我不想使用服务器GC,对吗?)。因此,为了使这个操作线性加速,我需要减少调用GC的次数。浪费的大部分时间实际上是由其他线程阻塞等待一个线程执行GC造成的。


重型计算部分用C或C++编写并作为非托管代码引用,这样GC就不会对其产生影响,这样做可能更快吗? - Jesus Ramos
你能分享一下让你认为时间花费在GC上的信息吗?另外,在开始操作之前,你尝试过使用GC.Collect吗?(也许是为了释放先前的结果集?) - Kieren Johnstone
1
诊断结果非常可疑。多线程子任务执行与单线程任务相同的计算,却产生更多的垃圾,这是没有道理的。除非代码出了问题。开始思考堆锁的影响,或许你会有所收获。 - Hans Passant
@HansPassant 请看一下我最后的编辑以获得澄清,我认为我的问题实际上是高频率的GC运行使我的代码有效地串行化了。在ANTS性能分析器中花费在GC上的百分比是从哪里获取的,我不知道它是否是一个可靠的数字。 - Anders Forsgren
@CodeInChaos 我会尝试一下,但我认为有一个不同的工作站GC是有原因的吧?问题在于这基本上是一个长时间的非交互式操作,在其他交互式应用程序中。如果我想仅针对此操作使用服务器版本,我需要一个新进程吗?我将尝试将GC的延迟模式设置为低延迟,以便在操作期间查看其效果。 - Anders Forsgren
显示剩余5条评论
4个回答

4

个人而言,如果你的任务只需要50毫秒就能执行完,那么线程创建等额外开销所需的时间可能比实际工作还要长,这也是你看到的情况。因此,你可能无法深入研究。

至于查看现有工具,我使用过的最好的工具是ANTS Profiler(内存和性能)。从那里,你可以看到内存中的对象以及时间点之间的差异,以及“执行次数”,这应该能够帮助你获得想要的结果。


是的,创建线程需要可测量的时间,这是许多人忽略的。使用线程池会更好一些,但仍然有成本。 - Kieren Johnstone
我使用TPL,所以池化应该被照顾到了。同时依据我的经验,50毫秒足以作为单独线程上的工作块。我已经使用过ANTS内存分析器,但是我没有找到一种方法可以查看已经被创建但不再活跃的对象。我使用ANTS性能分析器找出哪些构造函数被频繁调用。 - Anders Forsgren

1
也许你应该考虑增加对象之间的缓存命中率。
因此,与其创建新的结构体点,然后在列表/可枚举中执行计算,不如尝试分配一个固定大小的点数组,然后持续重复使用这些点。这样,你只需要一次性分配对象,执行计算,然后返回即可。如果能够完全重复使用数组,你将从热缓存中受益,并且不会遭受任何GC的影响。

顺便提一下,确保您也回应了 @Mitchel Sellers 的评论,您可能可以使用线程池而不是显式线程,但上下文切换开销是真实存在的,也会对性能产生影响。 - Spence
重要的部分仍然超过10,000行代码,因此我需要找出优化的重点,例如通过重复使用已分配的对象而不是在循环中进行分配,或者通过更改数据布局或使用不安全的代码。线程创建开销不是问题。 - Anders Forsgren
1
好的,你可以使用指针来进行优化,但我建议首先使用Redgate分析工具,找到代码中的热点,然后看看是否可以对它们进行优化。有时候代码中存在对性能有巨大影响的操作。最糟糕的情况是连续重新分配缓冲区,这会损害GC的性能。 - Spence
确切地说,我需要找到我正在进行GC工作的地方。这正是我所询问的:使用哪些工具以及如何使用。我已经使用过VS2010和Redgates分析器,但我无法看到我正在创建对象的位置。这里的一个问题是数据的“工作集”很快增长到200MB左右,而实际结果通常是50-100MB的数据,因为它代表输出,所以不能被GC回收。我担心我正在创建一百万个占用100MB的对象,这些对象在操作期间至少不应该被GC回收。 - Anders Forsgren

1

虽然这是一个旧问题,但对于那些遇到相同问题的人...

我曾经遇到完全相同的问题,并通过设置服务器模式垃圾回收 http://msdn.microsoft.com/en-us/library/ms229357(v=vs.110).aspx 来永久解决它。

app.config 中添加:

  <runtime>
     <gcServer enabled="true" />
  </runtime>

这已经将我的代码速度提高了一个数量级,没有任何副作用。如果你知道在哪里产生了大量的GC,我还发现LowLatency http://msdn.microsoft.com/en-us/library/system.runtime.gclatencymode(v=vs.110).aspx 可以将我的GC降至单个一代GC。
GC.Collect ' pre-emptively collect before time-critical region
Dim oldmode As GCLatencyMode = GCSettings.LatencyMode
RuntimeHelpers.PrepareConstrainedRegions()

Try
    GCSettings.LatencyMode = GCLatencyMode.LowLatency

    ' Work that allocates tons of memory here

Finally
    GCSettings.LatencyMode = oldmode

End Try

我希望PrepareConstrainedRegions能确保Finally块始终执行,但我并不完全确定这是正确的。

0

这些结果点对象。就像标准结构体Point一样吗? 从这里看不出来,但你尝试过为它们预分配空间吗?大多数GC调用可能会为它们分配内存,这需要很多工作量,如果能够以较大的块甚至一次性计算出数量,那么这样做应该会给你带来提升。

另一个选择可能是进入不安全代码,前提是你可以在工作站上获得该权限。 不知道你的点是如何布局的,但只需分配一个内存块,然后通过指针算术运算进行操作可能会有所发展。


即使将所有点分配到数组中,这也可能会有所帮助,并且不需要使用不安全模式。 - Martin Brown
结果数据中创建的很多对象都是坐标点(不是标准结构体,而是具有2个double的类似结构体)。我认为创建许多临时值类型对GC来说不是问题。至于unsafe / perf,我们已经在本地代码中完成了大部分重型数值计算。这些代码片段用于构造和分析这些结果,并且如果不使用“面向对象”进行操作,则它们将过于难以创建和维护。例如,如果可能的话,我希望保持对结构体数组进行分析,而不是对数组结构体进行分析等。 - Anders Forsgren
每次创建一个点对象都意味着调用gc来查找足够的连续内存,将其标记为已分配等。如果它们是对象,则每个对象都将在每个GC传递中进行检查。如果您可以以块的形式执行此操作,则必定会减少GC的工作量。更糟糕的是,2个双精度浮点数并不占用很多内存,因此除非您在非常紧密的循环中实例化它们,否则它们可能会散布在各处,您的GC时间可能正在进行碎片整理... - Tony Hopkinson

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