关于GC与显式内存管理性能的任何硬数据?

时间:2009-04-16 12:22:33

标签: performance memory-management garbage-collection

我最近阅读了丹·格罗斯曼的优秀文章“The Transactional Memory / Garbage Collection Analogy”。一句话引起了我的注意:

  

理论上,垃圾收集可以   通过增加来提高绩效   空间位置(由于   对象重定位),但在实践中我们   支付适中的性能成本   软件工程的好处。

直到那时,我的感觉总是很模糊。一遍又一遍,你会看到GC 可以更高效的说法,所以我总是把这个概念放在脑后。然而,在读完这篇文章之后,我开始怀疑了。

作为衡量对GC语言影响的实验,有些人拿了一些Java程序,跟踪执行,然后用显式内存管理替换垃圾收集。根据{{​​3}}上的this review of the article,他们发现GC总是较慢。虚拟内存问题使GC看起来更糟糕,因为收集器在此时经常接触比程序本身更多的内存页,因此导致大量交换。

这对我来说都是实验性的。是否有人,特别是在C ++环境中,与显式内存管理相比,是否对GC性能进行了全面的基准测试?

特别有趣的是比较各种大型开源项目如何在有或没有GC的情况下执行。有没有人听说过这样的结果?

编辑:请关注性能问题,而不是为什么GC存在或为什么有益。

干杯,

卡尔

PS。如果你已经拔出了火焰喷射器:我不是要取消GC的资格,我只是想对性能问题做出明确的答案。

16 个答案:

答案 0 :(得分:23)

这变成了另一个充满“我的直觉”的火焰战争。一些变更的硬数据(论文包含细节,基准,图表等):

http://www.cs.umass.edu/~emery/pubs/04-17.pdf说:

“结论。关于垃圾收集性能影响的争议长期以来一直掩盖了它所提供的软件工程效益。本文介绍了一种基于跟踪和仿真的oracular内存管理器。使用这个框架,我们使用这两种方法执行一系列未改变的Java基准测试。垃圾收集和显式内存管理。比较运行时,空间消耗和虚拟内存占用,我们发现当空间充足时,垃圾收集的运行时性能可以与显式内存管理竞争,甚至可以超过4%我们认为复制垃圾收集可以获得物理内存的六倍,与Lea或Kingsley分配器相比,可以提供相当的性能。“

当你有足够的内存时,复制GC变得比明确free() - http://www.cs.ucsb.edu/~grze/papers/gc/appel87garbage.pdf

更快

它还取决于您使用的语言 - Java必须在每个集合上进行大量重写(堆栈,对象,生成),并且编写一个不必在JVM中停止世界的多线程GC将是一个伟大的成就。另一方面,你几乎可以在Haskell中免费获得GC时间很少> 5%,而分配时间几乎为0.这真的取决于你在做什么以及在什么环境中。

答案 1 :(得分:17)

在垃圾收集内存模型中,内存分配的成本通常要低得多,然后只是显式使用new或malloc,因为垃圾收集器通常预先分配这个内存。但是,显式内存模型也可以这样做(使用内存池或内存区域);使内存分配的成本等于指针添加。

正如Raymond ChenRico Mariani指出的那样,托管语言在一般情况下倾向于执行非托管语言。但是,在推送之后,非托管语言可以并且最终将击败GC / Jitted语言。

同样的事情在Computer Language Shootout中也很明显,因为尽管C ++在大多数情况下往往比Java高,但你经常会看到C ++实现通过各种环节(如对象池)来实现最佳表现。但是,垃圾收集语言往往更易于遵循和更直接的实现,因为GC更善于分配(小块)内存。

然而,在GC与非GC方面,性能并不是最大的差异;可以说它是非GC(和引用计数)语言的确定性终结(或RIIA),它是显式内存管理的最大参数,因为它通常用于内存管理以外的目的(例如释放锁,关闭文件或窗口句柄)等等)。 '最近'但是C#引入了using / IDisposable构造来完成这个。

垃圾收集的另一个问题是它们使用的系统往往相当复杂以防止内存泄漏。但是,一旦发生内存泄漏,这也会使调试和跟踪变得更加困难(是的,即使是垃圾收集的语言也会有内存泄漏)。

另一方面,垃圾收集语言可以在最佳时间(或大约)处理最优化的事情,而不必使开发人员承担该任务的负担。这意味着为GC语言开发可能更自然,因此您可以更专注于真正的问题。

答案 2 :(得分:7)

这是我想要运行的实验:

  1. 启动一个用垃圾收集环境(如.NET或Java)编写的程序。
  2. 启动一个用非垃圾收集环境(如C或C ++)编写的类似程序。
  3. 使用程序并查看哪个程序更具响应性。
  4. 客观性的提高:让你的祖母做第3步。

    引用最佳GC实现的理论性能非常好,但实际情况是,在现实世界中,使用垃圾收集语言编写的程序的性能不如本机应用程序。这就是为什么性能直接转换为用户体验的大型项目仍然是用C ++编写的。典型的例子是游戏编程。

    另一个可能违反直觉的例子是Eclipse IDE。虽然它可以用Java 编写,但整个图形子系统必须重写以产生可接受的性能。解决方案:使GUI元素围绕本机(C / C ++)组件(SWT)进行轻量级包装。

    我理解垃圾收集环境的绘制。内存管理很难做到。还有很多工作要做。但最重要的是:了解程序的行为方式可以让您(程序员)优于machine trying to guess

答案 3 :(得分:6)

Berger的论文被引用了很多,但它将真实的垃圾收集器与纯粹的理论,离线,最优算法进行比较。因此,虽然它可能会告诉您一些关于理论限制的内容,但它对真实垃圾收集器的性能与malloc和{的真实实现的性能几乎没有关系。 {1}} 即可。我喜欢的一项研究更好地采用了真正的程序,并将明确的freemalloc与Hans Boehm的保守垃圾收集器进行了比较:

  Ben Zorn的

The Measured Cost of Conservative Garbage Collection

这项研究并不完美,Zorn谨慎地指出,如果程序知道他们使用的是垃圾收集器,那么有些可以更快。但硬数据是这样的:    - 在最初编写为使用freemalloc的实际程序中,垃圾收集版本以大约相同的速度运行但需要两倍的内存。 Zorn相当令人信服地说,如果你知道你有GC,你可以让事情变得更快,但是很难消除内存损失。

我从这项仔细的实验​​研究中学到了更多,而不是从伯杰对无法实现的理想化记忆管理者的研究中学到的。

答案 4 :(得分:2)

这里给出了相当多的不同论点。我想首先明确表示你不能真正进行1:1的比较。每个都有其优点和缺点,任何代码片段将更适合于一个或另一个系统。相反,那就是说,你必须知道你是否有GC来编写有效的代码。

我的论点是你必须了解你的环境和代码。这将使您的代码更有效。从一个范例转移到另一个范例并编写相同的样式将使您的代码比GC真正帮助/带走的效率更低。

案例:

程序为短期对象生成数千个堆内存分配。也就是说,它使用不同大小的对象分配和释放多次。

在非GC环境中,对于每个分配,您最终都会调用malloc,并且需要在可用内存片段列表中找到最合适的一个(根据特定的malloc实现)。使用内存然后使用free [或C ++中的new / delete ...]释放它。内存管理的成本是定位片段的成本。

在GC环境中,使用可移动的GC作为java或.net,在每次GC运行后,所有可用内存都是连续的。获取对象的内存的成本便宜,非常便宜(在Java VM中<10 cpu指令)。在每次GC运行时,只定位活动对象并将其移动到相应内存区域的开头(通常是与原始区域不同的区域)。内存管理的成本主要是移动所有可到达(活动)对象的成本。现在,前提是大多数对象都是短暂的,因此最终成本可能小于非GC系统的成本。在单个GC运行中分配和释放(遗忘)的一百万个对象无需额外费用。

结论:在GC语言中,您可以在堆上创建所有本地对象。它们很便宜。另一方面,在非GC系统中,一堆分配,解除分配和新分配是昂贵的。内存碎片化,malloc的成本增加......在非GC系统中,你应该尽可能多地使用堆栈,而不必使用堆。

这有另一个含义。习惯于两个内存系统之一的人往往会在另一个中编写低效的程序。它们习惯了一些在其他系统上可能不好的习语。

一个明显的例子是一个非托管程序员,用于分配一个对象并重用(根据需要用新元素重置其内部指针)用于这种思维方式:分配是昂贵的,重用是便宜的。现在,如果将相同的确切代码移动到世代GC环境(Java,.net - 都是移动生成GC),您将获得一个有趣的效果。在Java代际GC中,该系统仅对年轻一代执行次要集合,仅处理完整集合中的老一代。但是年轻一代中的一个物体可以被老一代的物体所引用,因此必须进行额外的工作来跟踪这些从古到老的参考。特别是在Java 1.4.1 garbage collector中,系统将标记旧对象所在的存储卡(页面的子部分),然后包含所有标记的卡片,以便在次要收集期间进行处理,增加GC必须执行的工作量并可能影响性能。

对象在1,2,3 ... GC运行期间仍处于活动状态,它被移动了很多次,最后被移动到老一代,在每次GC运行中它都不会被移动但是可以站在那里......但是,唉,程序员强迫对象变得更年轻。它再次移动,每次GC运行到再次变老时它将再次移动。

为了进行合理的比较,您需要让知道环境的程序员编写不同的代码片段,使用相同的算法解决相同的问题,并使用不同的内存管理思维集。然后比较两者的结果。

答案 5 :(得分:1)

GC总是比极端的替代方案慢:完美的,非确定性的内存管理。

问题是:

  • 这些差异是否足以狡辩?
  • 一种技术的缺点是否足以让我们认真考虑另一种?

托管子系统还有其他领域胜过非托管子系统:

一般情况下,多任务操作系统上的程序总是比单任务程序 - 或没有操作系统的计算机上运行速度慢。

通常,程序在具有虚拟内存的系统上的运行速度总是低于没有虚拟内存的系统。

除极端情况外,我们是否认真考虑没有虚拟机且没有操作系统的计算机系统?

答案 6 :(得分:1)

在讨论了另一个答案后,结果表明GC可以改变渐近复杂度。 Soru's comment在没有例子的情况下隐含地说明了这一点,并且一条评论不足以解释它。感谢Jon Harrop的例子,他建议并对这个答案提出有用的评论。然而,正如我最后解释的那样,一个优秀的GC应该仍然能够分摊成本(给予足够的内存,一如既往)。

  

使用GC系统时,当你的O(N)代码以病态的方式废弃GC时会导致O(堆大小),可能更难找出出错的地方。

首先,当工作集的大小接近最大堆大小时,通常会发生这种情况。过于频繁地调用GC,因此一切都变慢了。安装Eclipse的Scala插件,你会感觉到它。

然而,通常情况下,如果您的算法只能产生大量可快速检测到的垃圾,那么拥有足够的内存空间和分代GC会阻止它:它将无法存活足够长的时间才能离开托儿所。

这是一个例外的示例:让我们使用“map f list”,并假设f的每个应用程序通过在哈希表中保存对返回值的引用来消耗实时内存。这里的渐近复杂度应该仍然是O(1)。

通过收集托儿所,分代GC将无法释放内存,因此会定期调用主要集合(O(堆内容))。因此,我们得到运行时至少与(堆内容大小)* n *(每次调用f所消耗的空间)/(托儿所大小)成比例。

GC实际上会将托儿所的大小增加到指定的最大值,然后上面再次发生n大到足够大。但是,如果指定的最大值是Big-Theta(最大堆大小)并因此是Omega(堆内容大小),则主要集合变得不常见,并且次要集合的成本与生成的数据成比例(因此与生成所需的运行时间成比例)他们)。 这类似于您需要复制容器以调整其大小时:通过增加它,您可以分摊复制(或GC)的成本,并使其与导致复制所需的成本相当。

这整个答案没有考虑到暂停时间的问题 - 收集变得很少但很长。它隐含地假设堆栈扫描时间是不变的 - 但它应该确实存在,除非你的算法是非尾递归的,因此无论如何都要考虑这里考虑的大输入问题。

最后,它不是关于增量垃圾收集器。它们在根处解决问题,但它们主要用于实时GC,因为它们会增加内存读取的开销。 由于硬件支持以优化此开销,Azul System通过其Pauseless GC在他们自己的自定义硬件上解决了这个问题。他们最近声称也解决了x86的问题,请参阅this "press release"this article。但它肯定在开发中,并且不会在不改变操作系统的情况下运行。完成后,如果表现符合他们的建议,也许问题也将为我们正常的凡人解决。

答案 7 :(得分:0)

理论上,一个精心设计的程序可以通知智能GC子系统达到手动内存管理所描述的加速比。如果没有长时间运行,这些加速可能是不可见的,以分摊GC的固定启动成本。

在实践中,您可能无法通过当前的GC实现来实现这些加速。此外,你不会得到明确的答案,因为两种情况都会存在病态上不好的情况。

答案 8 :(得分:0)

一个实用的问题是,通过明确的MM,通常更容易分析,识别瓶颈并解决它。

使用GC系统时,如果你的O(N)代码以一种病态的方式废弃GC而使其成为O(堆大小),则可能更难找出出错的地方。有时甚至像解决内存损坏一样困难。

答案 9 :(得分:0)

gc和任何malloc实现之间的根本区别在于gc跟踪分配的对象,而malloc基本上跟踪 de 分配的对象(通过“免费列表”)等等,需要收集释放的内存块,以便在后面的malloc上快速返回它们) - 一些malloc实现甚至没有可能(在它们的内部)通过设计枚举所有分配的块。 因此,任何可能的gc实现(无论它有多好)都将具有复杂度O(somefunc(N)),其中N是分配对象的计数,而大多数malloc实现具有复杂度O(1)。因此,当(同时站立的)分配的对象的数量越来越多时,任何gc的性能降低是不可避免的(但是当然可以交换性能以用于额外的存储器消耗)。 [毕竟很明显,与分配的块/对象相比,空闲内存块总是没有维护开销。]这是任何gc的基本缺陷,因为它收集维护垃圾 :),malloc维护 nongarbage (:。

P.S。通过malloc,我的意思不仅仅是特定的所谓的C函数,而是任何显式的内存分配例程。 此外,我想指出,没有内置gc的编程语言提供了很多方法来包装显式的malloc / free(new / delete)调用(例如,C ++中的std :: shared_ptr或Obj-C中的ARC)这使程序代码看起来与gc驱动的语言类似,但从性能的角度来看,它与显式内存分配更接近(几乎相当)。 (我知道即使是简单的引用计数也可以被认为是垃圾收集的一种形式,但在这篇文章中,gc我的意思是任何一些功能更丰富的实现(至少自动检测参考周期),这需要跟踪所有分配对象,所以我不认为像std :: shared_ptr这样的包装器作为gc(至少在这篇文章中))。

答案 10 :(得分:0)

  

作为测量对GC语言的影响的实验,一些人使用了一些Java程序,跟踪了执行情况,然后用显式内存管理代替了垃圾回收。根据对Lambda Ultimate这篇文章的评论,他们发现GC总是较慢。

嗯,不。他们对在非生产型研究VM(Jikes)上运行的少数面向对象的Java程序进行了测量,这些VM使用了非常不寻常的GC算法,所有这些算法已经过时了数十年。然后,他们针对给定的输入对每个程序进行了概要分析,并找出了每个free可以插入的最早点(即,导致程序通常不再正确,因为在其他输入上它可能过早释放)。

我对此没有任何问题:他们的结果有点有趣,但并不令我惊讶(Java是一种非常糟糕的语言,与诸如Hotspot或。净)。但是,他们假装将这些结果归纳为绝对荒谬的结论。没有理由相信他们的玩具GC代表生产GC,或者OOP代表所有程序,或者它们的“常规”内存管理代表手动内存管理。

  

虚拟内存问题使GC看起来更加糟糕,因为此时收集器经常触摸比程序本身更多的内存页面,从而导致大量交换。

是的。不要那样做。

  

这对我来说都是实验性的。与显式内存管理相比,是否有人(尤其是在C ++中)对GC性能进行了全面的基准测试?

C ++无法表达有效的GC算法(例如,无法遍历该堆栈或更改调用约定),因此它永远不会具有竞争力。如果必须使用C ++,请不用担心GC。如果您想要一个不错的GC,那就不用C ++了。

  

特别有趣的是比较各种大型开源项目在例如有无GC的情况下的执行情况。有人听说过这样的结果吗?

这不可能以一种有用的方式来完成,因为是否存在GC是其他所有内容都基于的基本设计决策。

答案 11 :(得分:-1)

事实上,开发人员是人类,并且首先会错过导致垃圾收集器需求的东西。有了这个说,让我说垃圾收集总是比完美显式内存管理慢。并且垃圾收集可以通常比不完美的显式内存管理更快,因为垃圾收集器清理开发人员往往忘记的事情。

答案 12 :(得分:-2)

理论上,GC 可能在某些情况下更快,但我从未见过,而且我怀疑自己会不会。此外,带有GC的C ++(如Boehm GC)可能总是较慢,因为它是保守的。随着所有指针在C ++中摆弄,GC必须假装所有都是指针。使用像Java这样的语言,它可以准确地知道什么是指针而不是指针,因此它可能更快。

答案 13 :(得分:-2)

正如@dribeas指出的那样,(Hertz&amp; Berger)论文中对实验的最大“混淆”是代码总是在一些“隐含的假设”下编写,关于什么是便宜和什么是昂贵的。除了那种混乱之外,实验方法(离线运行Java程序,创建对象生命周期的神谕,回归'理想'的alloc / free调用)实际上非常精彩和有启发性。 (我个人的观点是,混淆不会对他们的结果造成太大影响。)

就个人而言,我的直觉是使用GC编辑的运行时意味着接受应用程序的三倍性能影响(GC将慢3倍)。但程序的真实情况充满了混乱,如果您可以在许多应用程序域中对许多程序执行“理想”实验,您可能会找到一个巨大的数据散点图,GC有时会赢,而手动通常会赢。 (而且情况正在不断变化 - 当多核(以及为多核设计的软件)成为主流时,结果是否会发生变化?)

另见我对

的回答

Are there statistical studies that indicates that Python is "more productive"?

的论点是“由于如此多的混淆,所有关于软件工程的证据都是轶事”。

答案 14 :(得分:-2)

附注:另一个有趣的实验,我没有看到人们试过,是与刚刚泄漏相比较。呼叫分配,永不免费。这是一个有趣的选择。

除了长时间运行的服务器应用程序,在实践中你永远不会耗尽内存,操作系统只会开始使用磁盘进行虚拟内存(机器实际上有无限的内存,达到虚拟地址空间的限制,我现在用64位机器认为是巨大的)。这突出表明GC只不过是一种改善局部性的设备。当你拥有无限的内存时,泄漏/死亡的对象不会“伤害”,除了内存来自层次结构,并且你想要将“实时”对象保持在附近并且快速并且将“死”对象保持在遥远/慢速内存中。如果每个对象都分配在不同的页面上,则OS虚拟内存系统将有效地成为GC。

答案 15 :(得分:-4)

另见

http://prog21.dadgum.com/40.html

讨论了“足够聪明”的编译器。 CS /软件的概念充满了理论/技术,理论上 但这都是蛇油。

GC今天很昂贵,而且可能永远都是。