繁体   English   中英

去GC还是不去GC

[英]To GC or Not To GC

我最近看了两个非常有趣且具有教育意义的语言讲座:

Herb Sutter撰写的第一篇文章介绍了C ++ 0x的所有出色功能,以及为什么C ++的未来比以往任何时候都更加光明,以及M $在这场比赛中被称为好人。 讨论围绕效率以及如何经常减少堆活动来提高性能。

另一位由Andrei Alexandrescu创作的影片激发了从C / C ++到他的新游戏改变者D过渡 D的大部分内容似乎都非常有动机和设计。 但是,令我感到惊讶的是,D推动了垃圾回收,并且所有类都是仅通过引用创建 更令人困惑的是, 《 D编程语言参考手册》这本书在有关资源管理的部分中特别指出以下内容:

垃圾收集消除了C和C ++中必需的繁琐且易于出错的内存分配跟踪代码。 这不仅意味着更快的开发时间和更低的维护成本,而且生成的程序经常运行得更快

这与Sutter关于减少堆活动的不断讨论相矛盾。 我非常尊重萨特(Sutter)和亚历山大(Alexandrescou)的见解,因此我对这两个关键问题感到困惑

  1. 并非仅通过引用创建类实例会导致大量不必要的堆活动吗?

  2. 在哪些情况下可以使用垃圾回收而不牺牲运行时性能?

要直接回答您的两个问题:

  1. 是的,通过引用创建类实例确实会导致大量堆活动, 但是

    一种。 在D中,您struct class struct具有值语义,并且可以执行类中除多态性以外的所有操作。

    b。 由于切片问题,多态性和价值语义从未很好地结合在一起。

    C。 在D中,如果您确实确实需要在一些性能关键的代码中在堆栈上分配一个类实例,而又不关心安全性的损失,则可以通过scoped函数毫不费力地进行操作。

  2. 在以下情况下,GC可以与手动内存管理媲美或比其更快:

    一种。 您仍然在可能的情况下在堆栈上进行分配(就像您通常在D中所做的那样),而不是依靠堆来进行所有操作(就像您通常在其他GC语言中所做的那样)。

    b。 您有一个顶级的垃圾收集器(尽管D的当前GC实现在过去的几个版本中已经进行了一些重大的优化,所以它看起来有些天真,所以它的表现还不如以前的糟糕)。

    C。 您主要是分配小对象。 如果您主要分配大型数组,而性能最终成为问题,则可能需要将其中一些切换到C堆(您可以访问C的malloc并在D中释放),或者,如果它具有作用域的生存期,则可以使用其他一些像RegionAllocator这样的分配器。 (目前正在讨论和完善RegionAllocator,以便最终将其包含在D的标准库中)。

    d。 您不太关心空间效率。 如果使GC运行得过于频繁而无法使内存占用量过低,则会降低性能。

在堆上创建对象比在堆栈上创建对象慢的原因是,内存分配方法需要处理诸如堆碎片之类的事情。 在堆栈上分配内存就像递增堆栈指针一样简单(恒定时间操作)。

但是,有了紧凑的垃圾收集器,您不必担心堆碎片,堆分配的速度可以与堆栈分配一样快。 D编程语言的“ 垃圾收集”页面对此进行了更详细的说明。

关于GC语言运行速度更快的断言可能是假设许多程序在堆上分配内存的频率比在堆栈上分配频率高得多。 假设使用GC语言可以更快地分配堆,那么您就可以对大多数程序的大部分进行优化(堆分配)。

1的答案:

只要堆是连续的 ,在堆上分配与在栈上分配一样便宜。

最重要的是,当您分配彼此相邻的对象时,内存缓存性能将非常出色。

只要您不必运行垃圾收集器,就不会损失任何性能 ,并且堆保持连续。

那是个好消息:)

答案2):

气相色谱技术有了很大的进步。 如今,它们甚至可以实时添加。 这意味着保证连续内存是一个策略驱动的,与实现有关的问题。

因此,如果

  • 您可以负担得起实时gc
  • 您的应用程序中有足够的分配暂停
  • 它可以使您的自由列表不受限制

您可能会获得更好的性能。

回答未解决的问题:

如果开发人员从内存管理问题中解脱出来,他们可能有更多时间花在代码的实际性能和可伸缩性方面。 这也是一个非技术因素

它既不是“垃圾收集”也不是“易于出错的”手写代码。 真正智能的智能指针可以为您提供堆栈语义,这意味着您永远不会键入“删除”,但您无需为垃圾回收付费。 这是Herb制作的另一个视频,它表明了这一点-安全又快速-这就是我们想要的。

要考虑的另一点是80:20规则。 您分配的绝大多数位置很可能是无关紧要的,即使您可以将那里的成本降低到零,也不会从GC中获得太多收益。 如果您接受这一点,那么使用GC所获得的简便性将取代使用它的成本。 如果您可以避免进行复制,则尤其如此。 D为80%的情况提供了GC,对于20%的情况提供了访问堆栈分配和malloc的权限。

即使您拥有理想的垃圾收集器,它仍然比在堆栈上创建东西要慢。 因此,您必须拥有同时允许两者的语言。 此外,使用垃圾回收器获得与手动管理的内存分配相同的性能的唯一方法(正确的方法)是使它使用与经验丰富的开发人员相同的方法来处理内存,并且在许多情况下会要求垃圾收集器在编译时做出决定,并在运行时执行。 通常,垃圾回收会使事情变慢,仅用于动态内存的语言会变慢,用这些语言编写的程序的执行可预测性会降低,而执行延迟会更高。 坦白说,我个人不知道为什么需要垃圾收集器。 手动管理内存并不困难。 至少在C ++中没有。 当然,我不会介意编译器生成可以为我清理所有事情的代码,但是目前看来这不可能。

在许多情况下,编译器可以将堆分配优化回堆栈分配。 如果您的对象没有逃脱本地作用域,就是这种情况。

在下面的示例中,一个不错的编译器几乎可以肯定会让x堆栈分配:

void f() {
    Foo* x = new Foo();
    x->doStuff(); // Assuming doStuff doesn't assign 'this' anywhere
    // delete x or assume the GC gets it
}

编译器所做的工作称为转义分析

同样,D 在理论上可以具有移动的GC ,这意味着当GC将您的堆对象压缩在一起时,通过改进缓存的使用可能会提高性能。 如杰克·埃德蒙兹(Jack Edmonds)的回答所述,它还可以消除堆碎片。 手动内存管理可以完成类似的操作,但这是额外的工作。

当高优先级任务未运行时,增量低优先级GC将收集垃圾。 高优先级线程将运行得更快,因为将不会进行内存分配。 这是Henriksson的RT Java GC的想法,请参见http://www.oracle.com/technetwork/articles/javase/index-138577.html

实际上,垃圾回收确实会降低代码速度。 它为您的代码增加了必须运行的程序的额外功能。 它还存在其他问题,例如,直到实际需要内存后,GC才会运行。 这可能会导致较小的内存泄漏。 另一个问题是,如果未正确删除参考,GC将不会拾取它,并再次导致泄漏。 我与GC有关的另一个问题是,它在某种程度上促进了程序员的懒惰。 我提倡在进入更高级别之前学习内存管理的低级概念。 就像数学一样。 您将学习如何求解二次方,或者如何首先手动求导数,然后学习如何在计算器上进行求算。 使用这些东西作为工具,而不是拐杖。

如果您不想降低性能,请对GC以及堆与堆栈的使用情况保持警惕。

我的观点是,当您进行常规过程编程时,GC不如malloc。 您只需从一个过程转到另一个过程,进行分配和释放,使用全局变量,然后声明一些函数_inline或_register。 这是C风格。

但是,一旦进入更高的抽象层,至少需要引用计数。 因此,您可以通过引用传递,计数并在计数器为零时释放它们。 这很好,并且在对象的数量和层次结构变得难以手动管理之后,优于malloc。 这是C ++风格。 您将定义构造函数和析构函数以递增计数器,然后进行修改复制,因此,一旦一方修改了共享对象的一部分,但是另一方仍然需要原始值,共享对象将被拆分为两部分。 因此,您可以在函数之间传递大量数据,而无需考虑是在此处复制数据还是在此处发送指针。 引用计数会为您做出这些决定。

然后是整个世界,闭包,函数编程,Duck类型,循环引用,异步执行。 代码和数据开始混合,您发现自己将函数作为参数传递的频率比普通数据高。 您意识到元编程无需宏或模板即可完成。 您的代码开始泛滥成灾,失去了坚实的基础,因为您在回调的回调内部执行某些操作,数据变得无根,事物变得异步,您沉迷于闭包变量。 因此,这是基于计时器的内存遍历GC唯一可行的解​​决方案,否则根本无法进行闭包和循环引用。 这是JavaScript方式。

您提到了D,但是D仍然是C ++的改进,因此您可以选择在构造函数,堆栈分配,全局变量(即使它们是各种实体的复杂树)中进行malloc或ref计数。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM