在同一个解决方案中使用C#和C++进行GC

14

我有一个解决方案,由若干个C#项目组成。为了快速运行,它是用C#编写的。垃圾回收开始成为一个问题——我们看到了一些100毫秒的延迟,希望能避免这种情况。

一个想法是逐个项目将其重新编写为C++。但是如果将C#与非托管C++结合使用,C++项目中的线程是否也会被垃圾回收冻结?

更新

感谢您的回复。实际上,这是一个可能会产生100毫秒延迟的应用程序。当时用C#构建可能是一个糟糕的决定,但当时必须快速启动和运行。

现在,我们正在使用Windows的Multimedia Timers每5毫秒触发一个事件。我们确实看到一些100+毫秒的间隔,并通过检查GC计数器确认这些间隔总是发生在垃圾回收期间。优化已经开启;已经以发布模式构建。


1
.Net的垃圾回收应该不会引起重大问题。你可能做错了什么;你能给我们更多细节吗? - SLaks
2
@SLaks:我相信你的意思是它不应该引起问题。;) - Sam Harwell
@SLaks: “collection should cause”...你的意思是“collection 不应该导致”吗? - jrista
1
我同意SLaks的观点。你需要开始研究C++的优化而不是重新设计。 - Sergey
4
那些说“垃圾收集不应该成为问题”的人,通常是从100毫秒不是问题的角度来看。事实上,在某些需要可靠的最小延迟的情况下,例如实时交易(我的工作),垃圾收集确实是一个问题,因为它会在执行时间中引入不可预测性,而这些时间可能需要非常准确。@Michael:你有研究过资源池吗?它可以是对抗垃圾收集的一种非常有效的武器。 - Dan Tao
显示剩余2条评论
7个回答

6
我在一家贸易公司担任.NET开发人员,我们像你一样关注100毫秒的延迟。当需要可靠的最小延迟时,垃圾回收确实可能成为一个重要问题。
话虽如此,我认为迁移到C++并不是一个聪明的举动,主要是因为它需要耗费大量时间。垃圾回收会在内存堆上分配了一定量的内存后进行。您可以通过最小化代码创建的堆分配量来大大减轻这个问题
我建议尝试查找应用程序中负责大量分配的方法。构造对象的任何地方都将成为修改的候选对象。对抗垃圾回收的经典方法是利用资源池:不要每次调用方法时都创建一个新对象,而是维护已经构造好的对象池,在每次方法调用时从池中借用并在方法完成后将对象返回给池。
另一个容易的优化方法是寻找代码中使用的任何ArrayListHashTable或类似的非泛型集合,这些集合会对值类型进行装箱/拆箱操作,导致完全不必要的堆分配。尽可能使用List<T>Dictionary<TKey, TValue>等替换它们(这里特别指的是值类型的集合,例如intdoublelong等)。同样,注意任何可能装箱值类型参数(或返回装箱值类型)的方法。
这些只是减少垃圾回收次数的一些相对较小的步骤,但它们可以产生很大的影响。付出足够的努力,甚至可以在应用程序的连续操作阶段(除了启动和关闭之外的所有阶段)完全(或至少近乎完全)消除所有第二代垃圾回收。我认为你会发现第二代回收才是真正的重头戏。
以下是一篇论文,概述了一家公司通过资源池化以及其他方法来最小化.NET应用程序中的延迟,并取得了巨大成功。

Rapid Addition利用Microsoft .NET 3.5框架构建超低延迟的FIX和FAST处理

因此,我强烈建议您研究修改代码的方法,以减少垃圾回收而不是转换为完全不同的语言。


听起来资源池化、性能分析以及可能升级到.NET 4.0并使用后台GC是一个不错的选择,而不是使用C++。谢谢你的帮助!你提供的白皮书很棒。你知道有没有其他更详细的资料(类似于白皮书中提到的那个函数调用,在幕后进行装箱和拆箱)?也就是说,有没有一份关于避免GC和资源池化的最佳实践清单?或许还有一份完整的不应该调用的函数列表? - Michael Covelli
@Michael:我希望有这样的列表存在,但不幸的是Rapid Addition没有公开他们的标准“不要调用”列表,而且我也不指望Microsoft会公开这样的列表(特别是因为随着列出的方法实现被修改,人们预计它会随时间改变)。正如其他人所说,你最好的选择是对你的代码进行分析,并找到那些可能发生意外内存分配的地方。如果你还没有这样做,我也强烈建议在调试应用程序时使用perfmon来监视你的GC计数。它可以非常有启发性。 - Dan Tao

4

首先,您尝试过进行分析以查看是否可以优化内存使用吗?一个好的起点是使用CLR分析器(适用于所有 CLR 版本至 3.5)。

为了解决小的性能问题而将所有内容重写为 C++ 是非常激烈的变化,这就像通过截肢来治疗割伤一样。


谢谢您的回复。在这里,C#可能不是正确的选择,但我们必须在X天内让它运行起来。因此,决定无法快速地用C++编写。但现在,我们已经满足了截止日期并且正在运行,我们有更多的时间来进行更改。因此,逐个项目在C++中重新编写是我们正在考虑的一个选项。我们看到一些100毫秒的延迟,这明显是由GC引起的。如果我们无法通过分析、预分配等方法消除这些问题,那么我们将考虑进行重写。 - Michael Covelli

4
你确定那100毫秒的延迟是由GC引起的吗?在你花费大量时间、精力和金钱将其重写为C++之前,一定要非常确定GC确实是你的问题。将托管代码与非托管代码结合使用也会带来自己的问题,因为你必须处理这两个上下文之间的封送。这将增加其自身的性能损耗,并且你的净收益最终很可能为零。
我建议你对C#应用程序进行分析,缩小你的100毫秒延迟具体来自哪里。这个工具可能有帮助: 如何使用CLR Profiler 关于GC的一些话
再谈一下.NET GC(或者实际上任何GC)。这一点并没有被说得足够多,但它是成功编写具有GC代码的关键因素:
拥有垃圾回收器并不意味着你不需要思考内存管理!
写出与GC相容的最佳代码所需的工作和麻烦比编写与非托管堆相容的C++代码要少...但是,你仍然必须了解GC并编写与其相容的代码。你不能完全忽略所有与内存管理相关的事情。你需要更少地担心它,但仍需要考虑它。编写与GC相容的代码是实现高性能且不会创建内存管理问题的重要因素。
下面的文章也应该有所帮助,因为它概述了.NET GC的基本行为(适用于.NET 3.5...很可能这篇文章对.NET 4.0来说不再完全有效,因为它的GC发生了一些关键变化...例如,在集合发生时不再阻塞.NET线程): 在Microsoft .NET Framework中进行自动内存管理的垃圾回收

谢谢您的回复。是的,不幸的是,每次出现延迟时,GC计数都会确认发生了垃圾回收。我们从未遇到过没有进行垃圾回收的延迟。我之前没有听说过.NET 4.0中的这个功能(在进行垃圾回收时不阻塞.NET线程)。如果这是真的,那真的可以帮助我们节省很多时间。您有相关的参考资料吗?谢谢! - Michael Covelli
2
这是一个:http://geekswithblogs.net/sdorman/archive/2008/11/07/clr-4.0-garbage-collection-changes.aspx。很棒,这可能真的对我们有所帮助。我还没有尝试过.NET 4.0测试版。有其他人尝试过在实际测试中比较GC性能吗? - Michael Covelli
我相信.NET 4.0已经不再是测试版了...VS2010的发布活动会在接下来的几周内进行吧?我已经在试用/实验中使用新的2010/4.0工具数月了,它非常稳定可靠。虽然我还没有使用新的GC进行低级别的操作,但我也没有遇到任何问题。 - jrista
只是出于好奇...暂停您的应用程序的集合是Gen2集合吗?还是gen0/1集合?如果是gen0/1集合暂停您的应用程序,我会非常惊讶...但如果是gen2集合,我就不会那么惊讶了。如果您得到了很多gen2集合,那可能是一个需要通过一些优化来解决的问题。 - jrista
主要是Gen2集合。感谢您的帮助,听起来使用.NET 4 gc以及分析和优化是正确的选择,而不是C ++。 - Michael Covelli
很高兴听到这个消息。 :) 你得到了这么多的Gen2集合,真的很有趣...应该非常罕见。希望对代码进行分析能够帮助你确定瓶颈所在。 - jrista

3

CLR垃圾回收在进行回收时不会暂停运行非托管代码的线程。如果非托管代码调用托管代码或返回到托管代码,则可能会受到回收的影响(就像其他托管代码一样)。


谢谢你的回复。我怀疑是这样,但我找到了一些引用说了不同的事情。你有参考资料来确认吗? - Michael Covelli
@Michael:你找到的参考资料可能与未托管的COM互操作相关,这不会阻止GC,如果我没记错的话。但是常规的未托管调用会阻止GC。 - John Feminella
1
@Michael http://msdn.microsoft.com/en-us/magazine/bb985011.aspx如果你仔细想一下,实际上没有一种安全的方式来暂停运行本地代码的线程以执行GC。CLR无法知道本地代码是否持有执行GC所需的资源,从而导致死锁(当你开始涉及托管API时可能会发生这种情况)。 - Logan Capaldo

1
如果100毫秒是个问题,我认为你的代码是任务关键型的。混合托管和非托管代码将有调用托管应用程序域和非托管空间之间的互操作开销。
GC已经非常优化,因此在这样做之前,请尝试分析您的代码并进行重构。如果您担心GC,请尝试设置线程优先级并最小化对象创建,并尽可能地缓存数据。在项目属性中也打开"优化代码"设置。

谢谢您的回复。您有关于Interop开销实际上是多少的数据吗?哪种方法最好? - Michael Covelli
通常情况下,如果你只是调用方法,开销是可以忽略不计的,请参考我在这里的回答:https://dev59.com/-EzSa4cB1Zd3GeqPjyBi#2310298如果你开始在托管和非托管类型之间进行数据封送,例如使用字符串和数组,开销将变得显著。请参阅此微软链接以获取更多信息:msdn.microsoft.com/en-us/library/ms998551.aspx - Fadrian Sudaman
使用设置为Cdecl的DllImport调用用C++编写的.dll中的代码怎么样?如果它只返回值、结构体和IntPtrs呢? - Michael Covelli
返回结构体可能比IntPtr或普通值有更多的开销。只是分享一下,我在最近的帖子上读到,一个基本的Interop调用(不考虑编组开销)将需要大约10-30条指令,在现代处理数百万条指令的CPU上几乎可以忽略不计。 - Fadrian Sudaman

1
一个想法是逐个项目用C++重写它。但如果你将C#与非托管的C++结合使用,那么C++项目中的线程是否也会被垃圾收集器冻结? 不会,因为C++代码运行在不同的线程上。C++堆和托管堆是不同的东西。
另一方面,如果你的C++代码做了很多new/delete操作,当堆变得碎片化时,你仍然会开始看到C++代码中的分配延迟问题。这些停顿可能比你在C#代码中看到的要严重得多,因为没有垃圾回收。当堆需要清理时,它只会发生在new或delete的调用内部。
如果你真的有一个紧密的性能需求,那么你需要计划在时间关键的代码中不从通用堆中进行任何内存分配。实际上这更像是C代码而不是C++代码,或者使用特殊的内存池和放置new。

谢谢您的回复。您有任何参考资料可以证实C++线程不会被GC冻结吗?我找到了一些相互矛盾的参考资料,只是想确认一下。 - Michael Covelli
@Michael:抱歉,没有参考资料。只是一般知识。我过去十年一直在用C++进行实时编码。Windows的C/C++堆不会移动活动对象或延迟释放对象,因此没有垃圾收集器。但这并不意味着每次新建/删除调用的成本不能巨大地变化,避免不确定性的唯一方式是在进入时间关键代码之前处理好所有内存分配。 - John Knoeller
感谢您的帮助。您说得对,转换到C++并不能自动解决问题,因为new/delete时间仍然可能会有很大的变化。最好先优化C#,将所有的new调用从时间关键区域移出,然后再考虑进行全面重写。 - Michael Covelli

1

.NET 4.0有所谓的后台垃圾回收,这与并发垃圾回收不同,可能是导致您问题的原因。Jason Olson在.NET Rocks Episode #517上与Carl Franklin和Richard Campbell谈论了这个话题。您可以在页面5上查看此处的转录。

我不确定仅升级到4.0框架是否会解决您的问题,但在重新编写所有内容之前,我想研究一下它肯定是值得花时间的。


太好了,谢谢。看起来使用.NET 4.0和一些性能分析可能是更好的选择,而不是重新编写。 - Michael Covelli

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