垃圾回收的演示比手动内存管理更快


23

我已经在很多地方读取(赫克,我甚至写了那么我自己),垃圾回收可能(理论上)比手工内存管理更快。

但是,展示要比讲讲难得多。
我从未真正看到过任何代码可以证明这种效果。

是否有人拥有(或知道在哪里可以找到)证明这种性能优势的代码?


5
GC的问题在于大多数实施方式都不是确定性的,因此2次运行可能会产生截然不同的结果,更不用说很难隔离要比较的正确变量了
棘手怪胎

@ratchetfreak:如果您知道任何例子(例如)在70%的时间内速度都比较快,那我也很好。至少就吞吐量而言,必须有某种方法可以将两者进行比较(延迟可能无法工作)。
Mehrdad

1
好吧,这有点棘手,因为您总是可以手动执行使GC胜过手动执行的一切操作。也许最好将其限制为“标准”手动内存管理工具(malloc()/ free(),拥有的指针,具有refcount的共享指针,弱指针,无自定义分配器)?或者,如果您允许自定义分配器(根据您采用的程序员的类型,分配器可能更实际或更不实际),请对分配给这些分配器的工作进行限制。否则,手动策略“在这种情况下复制GC的操作”始终至少与GC一样快。

1
“复制GC所做的事情”并不是指“构建自己的GC”(尽管请注意,从理论上讲,这在C ++ 11及更高版本中是可行的,它引入了对GC的可选支持)。我的意思是,正如我之前在同一条评论中所说的那样,“做的事情使GC比您手动完成的工作更具优势”。例如,如果类似Cheney的压缩在很大程度上有助于此应用程序,则可以手动实现类似的分配+压缩方案,并使用自定义智能指针来处理指针修复。另外,使用影子堆栈之类的技术,您可以用C或C ++进行根查找,但要付出额外的工作。

1
@Ike:没关系。明白为什么我要问这个问题吗?这就是我的问题的全部内容。人们提出了各种各样的解释,这些解释应该是有道理的,但是当您要求他们提供证明他们在实践中正确的论证时,每个人都会迷迷糊糊。这个问题的全部目的是要一劳永逸地表明这实际上可以在实践中发生。
Mehrdad

Answers:


26

请参阅http://blogs.msdn.com/b/ricom/archive/2005/05/10/416151.aspx,并通过所有链接查看Rico Mariani和Raymond Chen(微软的非常有能力的程序员)对决。雷蒙德将改进非托管服务器,雷科将通过优化托管服务器中的相同内容进行响应。

借助几乎为零的优化工作,托管版本的启动速度比手册快了很多倍。最终,手册击败了托管人员,但只能通过优化到大多数程序员都不想达到的水平。在所有版本中,该手册的内存使用情况均比托管版本好得多。


+1引用了一个带有代码的实际示例:),尽管正确使用C ++构造(例如swap)并不难,并且可能会很容易地使您达到性能方面的目标……
Mehrdad

5
您也许可以在表现上超过Raymond Chen。我有信心,除非他因为生病而无法参加比赛,否则我会努力的很多倍,而且我很幸运。我不知道他为什么不选择您会选择的解决方案。我确定他有这样做的理由
btilly

在这里复制了Raymond的代码,为了进行比较,我在这里编写了自己的版本。包含文本文件的ZIP文件在此处。在我的计算机上,我的运行时间为14毫秒,雷蒙德的运行时间为21毫秒。除非我做错了(可能),否则即使使用内存映射文件或自定义内存池(他确实使用过),他的215行代码也比48行实现慢50%。我的是C#版本的一半。我做错了吗,还是观察到同样的事情?
Mehrdad

1
@Mehrdad在这台笔记本电脑上拿出gcc的旧副本,我可以报告您的代码和他的代码都不会编译,更不用说对其做任何事情了。我不在Windows上的事实可能解释了这一点。但是,假设您的数字和代码正确。它们是否在拥有十年历史的编译器和计算机上执行相同的操作?(查看博客的撰写时间。)也许,也许不是。让我们假设他们是,他(作为C程序员)不知道如何正确使用C ++等。我们还剩下什么?
btilly 2013年

1
我们剩下一个合理的C ++程序,可以将其转换为托管内存并加快运行速度。但是在哪里可以优化C ++版本并进一步加速。当托管代码比非托管代码快时,通常会发生的一般模式就是我们所有人的共识。但是,我们仍然有一个好的程序员的合理代码的具体示例,该示例在托管版本中速度更快。
btilly 2013年

5

经验法则是没有免费的午餐。

GC消除了手动内存管理的麻烦,并减少了犯错误的可能性。在某些情况下,某些特定的GC策略是解决该问题的最佳解决方案,在这种情况下,您无需为此付出任何代价。但是在其他解决方案中,其他解决方案将更快。由于您始终可以从较低的级别模拟较高的抽象,但是不能反过来进行,因此可以有效地证明,在一般情况下,较高的抽象不可能比较低的抽象更快。

GC 手动内存管理的特例

要手动获得更好的性能可能需要大量工作或更多错误,但这是另一回事。


1
这对我来说毫无意义。举几个具体的例子:1)生产GC中的分配器和写障碍是手写的汇编程序,因为C效率太低,所以您如何从C中击败它; 2)消除尾部调用是优化的一个示例用高级(功能)语言完成的操作不是C编译器完成的,因此不能用C完成。堆栈移动是高级语言在C级别以下完成的另一示例。
乔恩·哈罗普

2
1)我必须查看要注释的特定代码,但是如果汇编器中的手写分配器/屏障更快,则可以使用手写汇编器。不确定与GC有什么关系。2)在这里查看:stackoverflow.com/a/9814654/441099重点不是某些非GC语言是否可以为您完成尾部递归消除。关键是您可以将代码转换得更快或更快速。某些特定语言的编译器是否可以自动为您执行此操作是为了方便起见。在足够低的抽象度下,您可以随时根据需要自己进行操作。
Guy Sirton

1
C语言中的尾部调用示例仅适用于函数调用自身的特殊情况。C无法处理函数尾部相互调用的一般情况。图灵tarpit落到汇编器上并假设开发时间无限长。
乔恩·哈罗普

3

容易构造一种人为的情况,在这种情况下,GC的效率要比手动方法高得多-只需安排垃圾收集器只有一个“根”,并且一切都是垃圾,因此GC步骤立即完成。

如果您考虑一下,那就是在垃圾回收分配给进程的内存时使用的模型。进程死了,它所有的内存都是垃圾,我们完成了。即使实际上,启动,运行和死亡的过程也不会留下任何痕迹,这比永久启动和运行的过程更有效率。

对于用带有垃圾回收的语言编写的实用程序,垃圾回收的优点不是速度而是正确和简单。


如果很容易构建一个人工示例,您介意展示一个简单的示例吗?
Mehrdad

1
@Mehrdad他确实解释了一个简单的例子。编写一个程序,使GC版本在退出之前无法进行垃圾回收。手动内存托管版本会比较慢,因为它明确跟踪并释放了内容。
btilly

3
@btilly:“在退出前,编写一个GC版本无法进行垃圾运行的程序。” ...首先,没有进行垃圾收集是由于缺少可运行的GC 而导致的内存泄漏,而不是由于存在 GC 而导致的性能提升!就像abort()在程序退出之前用C ++ 调用一样。这是毫无意义的比较。您甚至没有进行垃圾收集,只是让内存泄漏。您不能说垃圾收集的速度更快(或更慢),如果您不是从一开始就进行垃圾收集……
Mehrdad

举一个极端的例子,您必须使用自己的堆和堆管理定义一个完整的系统,这将是一个很棒的学生项目,但是太大而无法满足这个要求。通过编写一个分配和释放随机大小的数组的程序,您会做得很好,这种方法旨在减轻非gc内存管理方法的压力。
ddyer

3
@Mehrdad不是。这种情况是,GC版本从未碰到过要运行的阈值,而不是它不能在其他数据集上正确执行。对于GC版本,这将是非常有用的,尽管它并不是最终性能的良好预测指标。
btilly

2

应该考虑GC不仅仅是一种内存管理策略;它还对语言和运行时环境的整个设计提出了要求,这带来了成本(和收益)。例如,必须将一种支持GC的语言编译成一种形式,在这种形式下,指针不能从垃圾收集器中隐藏起来,并且通常只有通过精心管理的系统原语才能构造指针。另一个考虑因素是难以维持响应时间保证,因为GC强制执行了一些步骤,这些步骤必须被允许完成。

因此,如果您使用的语言是垃圾回收,并且将速度与同一系统中的手动管理的内存进行比较,那么即使您不使用垃圾回收,您仍然必须付出开销来支持垃圾回收。


2

更快是可疑的。但是,如果受硬件支持,它可以是超快,不易察觉或更快的。很久以前就有针对LISP机器的设计。有人将GC内置到硬件的内存子系统中,这样主CPU不知道它在那里。像许多后来的设计一样,GC与主处理器同时运行,几乎不需要暂停。更现代的设计是Azul Systems Vega 3机器,其运行Java代码的速度比使用专用处理器和无中断GC的JVM快。如果您想知道GC(或Java)的速度有多快,请使用Google。


2

我对此做了很多工作,并在此处进行了描述。我用C ++对Boehm GC进行了基准测试,使用malloc但没有释放,使用分配和释放以及使用freeC ++编写的自定义标记区域GC进行分配,而OCaml的普通GC运行的是基于列表的n皇后求解器。在所有情况下,OCaml的GC都更快。故意编写C ++和OCaml程序以执行相同顺序的相同分配。

当然,您可以重写程序以仅使用64位整数而无需分配来解决问题。尽管速度更快,但会失败。(这是为了预测我正在使用C ++内置的原型在研究新的GC算法的性能)。

我在行业中花费了很多年,将真正的C ++代码移植到托管语言中。在几乎每种情况下,我都观察到性能上的实质性改进,其中许多可能是由于GC胜过了手动内存管理。实际的局限性不是可以在微基准测试中完成的,而是可以在截止日期之前完成的,而基于GC的语言可以极大地提高生产率,我从未回过头。我仍然在嵌入式设备(微控制器)上使用C和C ++,但即使现在这种情况也正在改变。


+1谢谢。我们在哪里可以看到并运行基准代码?
Mehrdad

代码分散在该地方。我在此处发布了标记区域版本:groups.google.com/d/msg/…–
乔恩·哈罗普

1
在那里有线程安全和不安全的结果。
乔恩·哈罗普

1
@Mehrdad:“您消除了这种潜在的错误源吗?”。是。OCaml具有非常简单的编译模型,没有诸如逸出分析之类的优化。OCaml对闭包的表示实际上比C ++解决方案要慢得多,因此它应该List.filter像C ++一样使用自定义。但是,是的,您肯定可以取消某些RC操作。但是,我在野外看到的最大问题是,人们没有时间在大型工业代码库上手工执行这种优化。
乔恩·哈罗普

2
是的,一点没错。无需额外编写代码,但是编写代码并不是C ++的瓶颈。维护代码是。用这种附带的复杂性维护代码是一场噩梦。大多数工业代码库都是数百万行代码。您只是不想处理这些。我见过人们将所有内容都转换为shared_ptr仅用于修复并发错误。代码慢了很多,但是,现在可以了。
乔恩·哈罗普

-1

这样的例子必然具有不良的手动存储器分配方案。

假设最好的垃圾收集器GC。它在内部具有分配内存,确定可以释放哪些内存的方法以及最终释放内存的方法。这些在一起花费的时间比所有的时间少GC; 在其他方法上花费了一些时间GC

现在考虑一个使用与相同的分配机制的手动分配器,该分配器GCfree()调用只是预留要通过与相同的方法释放的内存GC。它没有扫描阶段,也没有任何其他方法。它必然需要更少的时间。


2
垃圾收集器通常可以释放许多对象,而不必在每个对象之后都将其置于可用状态。考虑从阵列列表中删除所有符合特定条件的项目的任务。从N个项目列表中删除一个项目是O(N);从N个列表中删除M个项目,一次为O(M * N)。一次删除列表中所有符合条件的项目为O(1)。
超级猫2014年

@supercat:free也可以收集批次。(当然,仅由于列表遍历本身,删除所有符合条件的项目仍然是O(N))
MSalters 2014年

删除所有符合条件的项目至少为 O(N)。您是正确的,free如果每个内存项都具有与之关联的标志,则可以在批处理模式下运行,尽管在某些情况下GC仍可以领先。如果一个人有N个引用,它们标识N个事物中的L个不同项,则删除不存在任何引用并合并其余部分的每个引用的时间为O(M)而不是O(N)。如果一个人有M个额外的可用空间,则缩放常数可能会很小。此外,在非扫描GC系统中进行压缩还需要...
超级猫

@supercat:好吧,这肯定不是O(1),因为您在第一条评论中的最后一句话表示。
MSalters 2014年

1
@MSalters:“什么会阻止确定性计划建立托儿所?” 没有。OCaml的跟踪垃圾收集器是确定性的,并使用苗圃。但这不是“手动”,我认为您滥用了“确定性”一词。
乔恩·哈罗普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.