去GC还是不去GC


70

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

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

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

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

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

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

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


2
当然,您的意思是“创建对象” :)
xtofl 2011年

6
7票赞成,3票赞成。我认为这是一个很大的问题!随它去!
David Heffernan's

1
我不得不承认,我对那个说M $是个好人的人不满意:)
Tom Zych

14
这是一个宗教问题,因为简单的答案是“永远不要给猴子核武器”。在98%的情况下(我刚拔了一个数字),内存管理应该是自动的,对于另外2%的情况,仍然是C ++。
c69 2011年

7
@ c69这是另外2%的问题。我们不允许问问题吗?
David Heffernan

Answers:


45

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

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

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

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

    C。在D中,如果您确实确实需要在一些性能关键的代码中在堆栈上分配一个类实例,而又不关心安全性的损失,则可以通过该scoped函数而不会造成不合理的麻烦。

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

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

    b。您有一个顶级的垃圾收集器(尽管当前的GC实现在过去的几个发行版中进行了一些重大的优化,所以D的当前GC实现虽然有些天真,但它的表现还不如以前那样差)。

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

    d。您不太在乎空间效率。如果您使GC的运行过于频繁而无法将内存占用保持在极低的水平,则性能将会受到影响。



22

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

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

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


啊哈!很有意思。我会调查的。谢谢!
2011年

全部都是为了使语言的默认行为适合(优化)到货运用例,对吗?这并不意味着某些算法在启用GC的情况下仍然表现更好,顺便说一下D是支持的。恕我直言,这种灵活的方法应该使更多的人满意。
2011年

8
GC效率不高,因为使用malloc的方式不正确。通过内存池之类的方法来管理内存将比GC快得多。
Pubby 2011年

7
@Pubby:GC可以在内部使用内存池。对于他们来说,将事物代代相传更为普遍(这非常有效,因为大多数对象都是短暂的)。GC的真正问题在于,与其他方法相比,它总体上倾向于使用更多的内存,并且降低了CPU缓存的局部性(并因此降低了速度)。
Donal Fellows,

对于压缩垃圾收集器来说,分配(但不是解除分配)非常快,但是该页面实际上并没有说D具有压缩垃圾收集器,只是说“现代垃圾收集器”正在压缩。实际上,我确定我在D的网站上读到某个地方,说它的GC不紧凑,但我找不到该参考文献。
Qwertie 2012年

13

1的答案:

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

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

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

那是个好消息:)

答案2):

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

因此,如果

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

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

回答未解决的问题:

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


因此,这完全取决于算法以正确顺序分配和访问数据的方式。
Nordlöw

@Nordloew:消除碎片将对实际需要的页面数量产生较小影响。访问顺序具有更大的作用,但不在此范围内。
xtofl 2011年

7
“只要堆是连续的,在堆上分配就和在堆栈上分配一样便宜。” -好吧,n在C ++中在堆栈上分配变量是一条机器指令。清理为零。
卡罗莉·霍瓦斯

1
是的,但是在DI打赌中,本地对象仍然在堆栈上创建。它们仅使编译器处理堆/堆栈之间的差异,就像在C ++中一样,编译器处理在哪个寄存器中进行处理。
Mooing Duck 2011年

@MooingDuck:我也有一种感觉,在大多数情况下,这样的决定可以由编译器而不是程序员来决定。您对D做这些事情有什么参考吗?
Nordlöw

4

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


5
智能指针只是引用计数,而引用计数只是穷人的GC。对于所有要增加/减少的计数,引用计数也具有性能成本(根据Boehm GC的一些旧研究,引用计数通常比跟踪GC慢)并且不处理周期。
dsimcha's

1
引用计数也会导致相当大的代码膨胀:简单的指针分配要么成为函数调用,要么被内联。两者都比简单的要大mov(尤其是如果内联了析构函数的代码时)。如果指针在线程之间共享,那么您甚至可能需要额外的代码来确保计数的增加/减少是原子的。
彼得·亚历山大

1
@dsimcha:我已经在这种风格上开发了一段时间了,使用justT*和可以非常容易地工作,其中just和scoped_ptr<T>被引用。
BCS

3
@dsmicha:大多数C ++代码根本不需要引用计数的共享指针...在大多数情况下,一个简单的unique_ptr就足够了,而不会造成任何开销。即使您必须使用引用计数的指针(在大多数情况下都可以轻松避免,但有时使用shared_ptr可以节省大量工作),但大多数时候您仍然不需要那么多的赋值或副本。
smerlin 2011年

4

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


3

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


这也是我目前的看法。感谢您的详尽回答。
2011年

“内存管理”并不难...没错-但是我在学习它时犯了很多错误,并且看到许多合作开发人员也这样做,将来我可能还会犯新的错误。“无内存管理”更容易:)
xtofl 2011年

@xtofl:您不认为学习它不仅给您带来困难,而且对事物的工作原理有很多有用的理解?例如,我有一个朋友,他拥有计算机科学博士学位,Java程序,而他甚至不知道(几乎)Java中的所有内容都是动态分配的?而且他不了解指针与堆栈上的对象的关系。我非常感谢汇编程序,C和C ++的实际激励,以使我学习计算机的工作原理和生活。

1
@弗拉德:但是他真的需要知道这些吗?
GManNickG 2011年

2
@GMan:我完全同意你的看法。在美国两所最受人尊敬的IT大学之一获得博士学位后,您会认为您知道计算机的工作原理,因此他报名参加了低级工作,因为低级的事情很重要并且失败很严重。如果您坚持自己的域名-可以。不知道那些事情不仅可以,而且是现实。例如,我不知道计算机是如何由零建造到“完成”的,如果您将100年前寄给我,我将不得不打扫厕所以谋生... :-(

3

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

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)的回答所述,它还可以解决堆碎片问题。手动内存管理可以完成类似的操作,但这是额外的工作。


2
嗯。我的工具会将其标记为内存泄漏。...您是在此处编写的D还是C ++?
xtofl 2011年

@xtofl比方说带有GC的C ++。虽然这个概念很笼统。
mpartel 2011年

1
事实上,除非编译器知道,无论Foo的构造也doStuff将导致引用x或任何内部它被泄露,也不能做出这样的优化。在D中,编译器会知道两个函数是否都为pure,因为这保证了不会访问任何可变模块或静态变量,但是在大多数语言中,编译器必须检查这些函数的主体(大多数编译器不会这样做) ),因为在这两个函数中的任何一个函数中,可能已为全局变量或类变量分配了内部值x(包括x其自身)。
乔纳森·M·戴维斯

1
出于兴趣,实际上哪个C ++编译器进行了优化?
史蒂夫·杰索普

2
不要使用C链接器,因此它所做的链接时间优化与C或C ++通常所做的一样多。如果一个函数为pure,则D编译器可以知道没有引用在转义该pure函数,因为函数无法访问任何可变的静态变量或模块变量,但无论如何它仍然不会进行这种优化。从理论上讲可以,但是我不相信任何D编译器都可以将类放在栈上作为优化。
乔纳森·M·戴维斯


1

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

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


GC(至少是某些类型的GC)的要点之一是,正确删除引用包括对其进行覆盖。请注意,即使在非GC区域中,也无法做到这一点是一个错误(悬空的指针)。
BCS

@BCS当然,这正是我所指的。我见过许多程序员只是悬而未决,但在非GC语言中,我却很少见。您没有可以告诉您可以更轻松地使用内存管理的安全保护罩,因此您通常会更加谨慎。这与我对懒惰的观点紧密相关!
MGZero 2011年

0

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

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

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

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

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.