这个问题长期困扰着我。我来自一个沉重而长期的C ++背景,自从我开始使用C#进行编程并处理垃圾收集时,我总觉得这样的“魔术”会付出代价。
我最近开始使用Java编写的大型MMO项目(服务器端)。我的主要任务是优化内存消耗和CPU使用率。每秒发送数十万条消息,同时创建相同数量的对象。经过大量的分析后,我们发现VM垃圾收集器占用了大量的CPU时间(由于常量收集),并决定尝试最小化对象创建,在适用的情况下使用池并重用我们可以做的一切。到目前为止,这已被证明是一个非常好的优化。
所以,从我所学到的,拥有一个垃圾收集器很棒,但你不能只是假装它不存在,你仍然需要关注对象创建及其含义(至少在Java中)以及像这样的大型应用程序。)
那么,.NET也是如此吗?如果是,在多大程度上?
我经常写这样的函数对:
// Combines two envelopes and the result is stored in a new envelope.
public static Envelope Combine( Envelope a, Envelope b )
{
var envelope = new Envelope( _a.Length, 0, 1, 1 );
Combine( _a, _b, _operation, envelope );
return envelope;
}
// Combines two envelopes and the result is 'written' to the specified envelope
public static void Combine( Envelope a, Envelope b, Envelope result )
{
result.Clear();
...
}
如果有人已经制作了一个可以重复用来存储结果的信封,我会提供第二个功能,但我发现这有点奇怪。
当我更喜欢使用类时,我有时会编写结构,因为我知道会有成千上万的实例不断被创建和处理,这对我来说真的很奇怪。
我知道作为.NET开发人员,我不应该担心这类问题,但我对Java和常识的经验告诉我应该这样做。
对此事的任何亮点和想法都将非常感激。提前谢谢。
答案 0 :(得分:9)
是的,.NET也是如此。我们大多数人都忽略了内存管理的细节,但在你的情况下 - 或者在高容量导致内存拥塞的情况下 - 那么需要进行一些优化。
您可能会考虑对您的案例进行一次优化 - 我一直在考虑写一篇文章,实际上是结构和ref
的组合,用于真正的确定性处理。
由于你来自C ++背景,你知道在C ++中你可以在堆上实例化一个对象(使用new关键字并获取指针)或者在堆栈上实例化(通过实例化它就像一个基本类型,即MyType myType;
)。您可以通过告诉函数接受引用(在声明中的参数名称之前使用&
关键字),通过引用函数和方法来传递堆栈分配的项目。这样做可以将堆栈分配的对象保留在内存中,只要分配它的方法仍在范围内;一旦它超出范围,对象就会被回收,破坏者被称为ba-da-bing,ba-da-boom,Bob的你的叔叔,所有这些都没有指针。
我使用这个技巧在我的C ++时代创建了一些令人惊讶的高性能代码 - 牺牲了更大的堆栈和堆栈溢出的风险,当然,但仔细的分析设法将风险保持在极小。
我的观点是你可以使用结构和引用在C#中做同样的技巧。权衡?如果您不小心或者使用大型对象,除了堆栈溢出的风险之外,您不能继承,并且紧密耦合代码,使其不易测试且难以维护。此外,无论何时使用核心库调用,您仍然需要处理问题。
不过,在您的情况下,可能值得一看。
答案 1 :(得分:5)
这是一个很难以一种对你有帮助的方式确定明确答案的问题之一。 .NET GC 非常善于根据应用程序的内存需求进行调整。在不需要担心内存管理的情况下编译应用程序是否足够好?我不知道。
你肯定可以采取一些常识性措施来确保你不会锤击GC。使用值类型绝对是实现此目的的一种方法,但您需要注意不要在编写不良的结构中引入其他问题。
在大多数情况下,我会说GC会很好地管理所有这些东西。
答案 2 :(得分:3)
我见过太多的案例,人们会“优化”代码中的垃圾,而不用担心它的编写效果如何,或者它的工作效果如何。我认为应该首先考虑使代码解决手头的业务问题。代码应精心制作,易于维护和正确测试。
毕竟,如果测试表明需要优化,则应考虑优化。
答案 3 :(得分:3)
随机建议:
有人提到将死对象放入队列中以便重用,而不是让GC在他们身上......但是要小心,因为这意味着GC在整合堆时可能有更多的垃圾移动,并且实际上可能没有帮你。此外,GC可能已经在使用这样的技术。此外,我知道至少有一个项目,工程师试图汇集,这实际上损害了性能。很难对GC有一个深刻的直觉。我建议使用合并和非合并的设置,这样你就可以随时测量两者之间的穿孔差异。
您可能在C#中使用的另一种技术是针对性能不佳的关键部分下载到本机C ++ ...然后在C#或C ++ / CLI中使用Dispose pattern来保存不受管理的托管对象资源。
另外,请确保在使用不使用它们的值类型时,隐式地将它们打包并将它们放在堆上,这可能很容易来自Java。
最后,一定要找到一个好的内存分析器。
答案 4 :(得分:1)
我一直都有同样的想法。
事实是,虽然我们被教导要注意不必要的CPU节拍和内存消耗,但我们的代码中的小缺陷成本在实践中可以忽略不计。
如果你意识到并且经常观看,我相信你可以写出不完美的代码。如果您已经开始使用.NET / Java并且没有低级编程的经验,那么您可能会编写非常滥用且无效的代码。
无论如何,正如他们所说,过早的优化是万恶之源。您可以花费数小时优化一个小函数,然后事实证明代码的其他部分会产生瓶颈。只是保持平衡,简单地做,愚蠢地做。
答案 5 :(得分:1)
虽然垃圾收集器在那里,但糟糕的代码仍然是错误的代码。因此,作为.Net开发人员,我会说你应该仍然关心你创建了多少个对象,更重要的是编写优化代码。
我在环境中的代码评论中看到相当多的项目被拒绝,我坚信这很重要。
答案 6 :(得分:1)
.NET内存管理非常好,并且能够以编程方式调整GC,如果您需要的话。
我喜欢你可以通过继承IDisposable并根据你的需要调整它来在类上创建自己的Dispose方法。这非常适合确保始终清除与网络/文件/数据库的连接而不会泄漏。还有担心过早清理。
答案 7 :(得分:1)
您可以考虑编写一组对象缓存。您可以在某处保留可用对象的列表,而不是创建新实例。它可以帮助你避免GC。
答案 8 :(得分:1)
我同意上面提到的所有要点:垃圾收集器很棒,但它不应该用作拐杖。
我在代码评论中经历了许多浪费时间,讨论了CLR的细节。最明确的答案是在您的组织中发展绩效文化,并使用工具主动分析您的应用程序。将出现瓶颈,并根据需要进行解决。
答案 9 :(得分:1)
我认为你回答了自己的问题 - 如果它成为一个问题,那么是的!我不认为这是一个.Net与Java的问题,它真的是“我们应该采取特殊的长度来避免做某些类型的工作”的问题。如果你需要比你更好的性能,并且在进行一些分析之后你发现对象实例化或垃圾收集需要花费大量时间,那么现在是时候尝试一些不寻常的方法(比如你提到的池)。
答案 10 :(得分:1)
我希望自己是一个“软件传奇人物”,可以用自己的声音和口气说出这一点,但由于我不是,我依靠SL来做这些事情。
我建议安德鲁·亨特在.NET GC上发表以下博客文章会有所帮助:
http://www.simple-talk.com/dotnet/.net-framework/understanding-garbage-collection-in-.net/
答案 11 :(得分:1)
即使超出性能方面,修改传入的可变对象的方法的语义通常比基于旧的返回新的可变对象的方法的语义更清晰。声明:
munger.Munge(someThing, otherParams);
someThing = munger.ComputeMungedVersion(someThing, otherParams);
在某些情况下可能表现相同,但前者做一件事,后者会做两件事 - 相当于:
someThing = someThing.Clone(); // Or duplicate it via some other means
munger.Munge(someThing, otherParams);
如果someThing
是宇宙中任何地方,对于特定对象的唯一引用,则将其替换为对克隆的引用将是无操作,因此修改传入的对象将是相当于返回一个新的。但是,如果someThing
标识了存在其他引用的对象,则前一个语句将修改所有这些引用标识的对象,并将所有引用附加到其上,而后者将导致someThing
变得“超脱”。
根据someThing
的类型及其使用方式,其附件或分离可能没有实际意义。如果某个持有对象引用的对象可以在存在其他引用的同时修改它,则附件将是相关的。如果永远不会修改对象,或者可能存在someThing
本身之外的引用,则附件没有实际意义。如果可以证明后一个条件中的任何一个都适用,那么用引用新对象替换someThing
就可以了。但是,除非someThing
的类型是不可变的,否则这样的演示需要文档超出someThing
的声明,因为.NET没有提供标注的方法来注释特定的引用将识别一个对象 - 尽管它是可变类型的 - 没有人被允许修改,也没有注释特定引用应该是宇宙中识别特定对象的任何地方的唯一。