要GC还是不要GC

To GC or Not To GC

本文关键字:GC      更新时间:2023-10-16

我最近看到了两个非常好的和有教育意义的语言演讲:

这是Herb Sutter的第一个,展示了c++ 0x的所有漂亮和酷的功能,为什么c++的未来看起来比以往任何时候都更加光明,以及M$如何被认为是这个游戏中的一个好人。该演讲围绕效率以及最小化堆活动如何经常提高性能展开。

另一个,由Andrei Alexandrescu,激发了从C/c++到他的新游戏规则改变者D的转换。《D》的大部分内容似乎都具有良好的动机和设计。然而,有一件事让我感到惊讶,即D推动垃圾收集,所有类都是通过引用单独创建的。更令人困惑的是, D编程语言参考手册资源管理一节中特别指出了以下内容,引用:

垃圾收集消除了繁琐的、容易出错的内存分配跟踪代码在C和c++中是必需的。这不仅意味着更快的开发时间和更低的成本维护成本,但结果程序经常运行!

这与Sutter经常谈论的最小化堆活动相冲突。我非常尊重萨特和亚历山大的见解,所以我对这两个关键问题感到有点困惑

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

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

直接回答你的两个问题:

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

    。在D中,有structclassstruct具有值语义,可以做类所能做的一切,除了多态性。

    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技术进步很大;现在它们甚至有实时口味。这意味着保证连续内存的是一个策略驱动的、依赖于实现的问题。

如果

  • 你可以负担得起实时gc
  • 应用程序中有足够的分配暂停
  • 它可以保持你的自由列表为自由块

你可能会有更好的表现。

未问问题的答案:

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

这不是"垃圾收集"或"乏味的容易出错"的手写代码。真正聪明的智能指针可以为您提供堆栈语义,这意味着您永远不会键入"delete",但您不需要为垃圾收集付费。这是赫伯的另一个视频,说明了这一点——安全和快速——这就是我们想要的。

需要考虑的另一点是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++风格。您将定义构造函数和析构函数来增加计数器,您将在修改时复制,因此共享对象将分裂为两部分,一旦它的某些部分被一方修改,但另一方仍然需要原始值。所以你可以将大量的数据从一个函数传递到另一个函数,而不用考虑这里是否需要复制数据,或者只是在那里发送一个指针。refrefcounting会为你做这些决定。

然后是一个全新的世界:闭包、函数式编程、鸭子类型、循环引用、异步执行。代码和数据开始混合,你会发现自己传递函数作为参数的次数比传递正常数据的次数要多。您意识到元编程可以在没有宏或模板的情况下完成。你的代码开始变得虚无缥缈,失去坚实的基础,因为你在回调的回调中执行一些东西,数据变得无根,事情变得异步,你沉迷于闭包变量。因此,这就是基于计时器的内存遍历GC是唯一可能的解决方案的地方,否则闭包和循环引用根本不可能。这是JavaScript的方式。

你提到了D,但D仍然改进了c++,所以malloc或refcounting在构造器,堆栈分配,全局变量(即使它们是各种实体的复杂树)可能是你选择的。

相关文章:
  • 没有找到相关文章