垃圾回收何时比手动内存管理更快?

22
在什么情况下,垃圾回收比手动内存管理更有效?(这里手动可以是指在C中使用malloc和free,或者由C++普及的更干净的RAII和智能指针技术)
我喜欢垃圾回收可以从编写软件中消除一些意外的复杂性,但我更喜欢RAII和智能指针能够消除该复杂性的同时还可以处理除内存之外的资源,具有确定性、提供性能保证并且总体上更为高效。因此,我认为我可以安全地忽略垃圾回收。然而,我注意到人们一直在说垃圾回收比C++中紧密的资源管理更快,例如当有大量可用内存时。
那么,在什么情况下,垃圾回收可以胜过手动内存管理呢?我喜欢RAII和智能指针,但如果垃圾回收更快,我也很乐意接受它作为另一个工具。

6
有时垃圾收集可以更快,这是因为从未需要释放任何内容。如果您的程序不使用大量内存资源,则垃圾回收器可能永远不需要启动。在垃圾回收系统中,分配器可能只需移动指针就能完成。 - Michael Burr
GC的性能高度依赖于所选择的算法。手动内存管理器也是如此。最好的情况下,我们在比较苹果和橙子。 - Zach Saw
1
@Michael Burr:在这种情况下,手动管理可能同样便宜。您实际上正在描述一种情况,即在一整个分配批次被释放之前,程序终止了。无论是 GC 还是 malloc/free 实现都可以做到这一点。 - MSalters
1
我真诚地认为你问错了问题。更好的问题应该是,“垃圾回收器是否符合或超过了我的应用程序的性能预算,并且垃圾回收器有哪些优势?” - Conrad Frix
@MSalters:如果所有的分配/释放代码都在您的控制下,那么这是正确的,但是如果您正在使用库代码(无论是自己的还是别人的)或者使用RAII,您通常会在某种程度上失去对释放策略的控制。无论如何,我将其放在注释中,因为我认为这并不是一个重要的性能考虑因素。它可能以某种理论方式在较小的程序/实用程序中发挥作用,但我认为在这些情况下,它不会带来任何真正的性能“胜利”。 - Michael Burr
显示剩余2条评论
3个回答

13

垃圾回收的优点:

  • 通过增加指针来分配内存,而堆分配器必须采取对策以避免堆碎片化
  • 在现代处理器上,垃圾回收可以提高CPU缓存局部性能力,这是十分重要的
  • 垃圾回收不需要额外的代码来释放内存,泄漏概率较低
  • 一代垃圾收集器可以并发地进行垃圾回收,在多核CPU上管理内存接近于免费。

垃圾回收的缺点:

  • 难以使语言中将指针作为一等类型的效率更高。
  • 由于收集延迟,使用更多的虚拟内存空间。
  • 对操作系统资源(除了内存之外)的泄漏抽象较弱
  • 在某些情况下会导致程序操作暂停,因为正在进行垃圾回收。

在性能方面,垃圾回收技术毫不费力地击败堆分配器。Rico Mariani和Raymond Chen之间的“中文词典编程比赛”经常被引用,概述在此处. Raymond的C++程序最终获胜,但只有在多次重写并放弃使用标准C++库后才实现了这一点。


3
除非使用标记-清除-整理算法(即MS .NET GC),否则GC不能简单地移动指针来分配内存。在非C++/CLR环境(即传统的C++)中,您将无法执行整理部分。 - Zach Saw
1
另外,注意代际 GC 是一种与并发 GC 相互独立的概念。 - Zach Saw
1
当页面不再使用时,GC仍然需要将内存释放给操作系统。同样,堆内存管理器也可以保留这个未使用的页面以备将来使用。 - Zach Saw
垃圾回收(GC)与良好的堆内存管理器(例如低碎片化Win32堆内存管理器)相比,并不能改善缓存局部性。然而,它可以提高数据局部性,从而减少页面错误,提高性能。 - Zach Saw
6
你的结论最多是误导性的。 Mariani/Chen的比较中很少涉及到内存管理。其中一点点关于内存管理的内容只是因为当时的C++没有“move”构造函数(现在已经有了)。最终,除了作为后来击倒的稻草人之外,几乎没有任何明智的人会编写类似Raymond Chen初始版本(或大多数中间步骤)的代码。最后,所讨论的代码属于足够奇怪的任务,以至于它是否真正对任何人有意义还是值得怀疑的。 - Jerry Coffin
3
这个回答受到了我所预料的许多反感。你可以随意抨击Chen是一个“糟糕的程序员”,但这完全错过了重点。他最终确实制作出了一个打败Mariani版本的程序。关键是他花费了多个版本和数周的时间才做到这一点。托管代码只是提高生产力的工具而已。至于C++在停滞不前多年后获得了移动语义,也许你应该考虑感谢强有力的竞争 :) - Hans Passant

12

从理论上讲,我可以证明永远不可能。

首先,我们假设在任何情况下都使用最佳算法。使用次优算法会证明任何事情。

其次,假设最佳GC算法使用时间T0...Tn来决定是否应该在某个时刻释放内存分配i。总共是Σ(Ti)

现在,存在一个等效的手动内存管理算法,它使用相同的GC算法,但仅考虑已手动标记为释放的内存块。因此,运行时间为Σ(δiTi)(其中δi=0当未释放块i时,=1当它被释放时)。

显然,Σ(δiTi) ≤ Σ(Ti):有一个手动算法严格不劣于GC算法。

这与其他答案有何不同?

  • "使用RAII,您必须在每次分配时进行解除分配。" - 不,这是错误的假设。我们应该比较批处理或非批处理操作。如果每次超出作用域之外都要进行GC运行,那么GC将更糟糕。
  • "改进局部性" - 好吧,除非您忽略非GC语言可以更经常地使用堆栈,而且堆栈具有出色的引用局部性。
  • "减少内存泄漏的概率" - 这是完全正确的,但我们正在讨论运行时性能。(顺便说一句:很好指出这是低但非零的概率)。

3
你只是在论述内存释放(free)方面的数学模型,怎么能证明垃圾回收机制整体上不如此? - Zach Saw
1
@Zach Saw:对于等价证明,你必须在两种情况下使用相同的分配机制。因此,分配(可能不够明确)也得到了解决。 - MSalters
1
@MSalters:“只考虑手动标记为释放的内存块”-您没有考虑首先进行此标记所需的时间,这段时间在GC语言中根本不需要花费。难道这不需要解决才能使此证明有效吗? - antinome
1
@MSalters:“假设我们在任何情况下都使用最佳算法”(“最佳”是什么意思?),你不能“在两种情况下使用相同的分配机制” - 这根本没有意义。 GC分配时间是指针碰撞与标记-清除-压缩。手动内存管理需要更复杂的分配策略。 - Zach Saw
2
@MSalters:你在这里提出的数学是完全错误的,因为最初的假设就是错误的。 - Zach Saw
显示剩余9条评论

2

我知道一种情况,在传统的C++实现中,GC指针比智能指针(引用计数指针)快得多,假设GC使用相同的底层堆内存管理器(不是C++/CLR,因为我们没有像Mark-Sweep之后压缩内存这样的奢侈条件,并且我们正在尽可能地进行苹果与苹果的比较),前提是对象赋值时间显着超过对象创建时间。

例如排序、数组反转等。

在这里查看我使用传统的C++实现的GC框架和引用计数指针的测试的更多信息。数组反转测试的结果是GcString比ref counted String快16.5倍。

这可能归因于引用计数指针中痛苦缓慢的总线锁语义(除非您只针对纯单线程应用程序,否则需要锁来确保线程安全)。从我在传统的C++中实现高性能精确GC的经验来看,与'manual'(根据OP的定义手动)内存管理(即智能指针)相比,GC有更多的优化机会。


你应该使用原子操作来处理引用计数,而不是锁。 - GManNickG
@GMan:原子操作不会锁定,除非您在助记符前加上锁定前缀。或者您是建议使用隐式锁定操作码,例如xchg?无论哪种方式,它们都是锁定。 - Zach Saw
3
“Lock”通常被定义为操作系统锁,而原子操作可能被视为“CPU锁”。无论哪种情况,类别都不是重点,重点是实现。操作系统锁是过度的(如果必要,唯一适当的是自旋锁)。 - GManNickG
@GMan:哦,我现在明白你想建议什么了。你是在想引用计数指针是否使用了临界区?哈哈... - Zach Saw
1
就像我所说的,“lock”通常被定义为“操作系统锁定”,而你会使用“原子操作”这个术语来表示“CPU‘锁定’”。 - GManNickG

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