GC或不GC
我最近看到两个非常好的教育语言会谈:
Herb Sutter 首先介绍了C ++ 0x的所有优秀特性,为什么C ++的未来似乎比以前更加光明,以及M $如何被认为是这个游戏中的一个好人。 这个话题围绕着效率展开,以及如何最小化堆活动通常会提高性能。
另一个由Andrei Alexandrescu推动,从C / C ++转换到他的新游戏改变者D。 D的大部分东西看起来非常有动力和devise。 但是,有一件事让我感到惊讶,就是D推动垃圾收集,所有的类都是通过引用创build的 。 更令人困惑的是,在“ 资源pipe理 ”部分特别指出了“D编程语言参考手册”中的下列内容:
垃圾回收消除了在C和C ++中所需的单调乏味,容易出错的内存分配跟踪代码。 这不仅意味着更快的开发时间和更低的维护成本,而且最终的程序运行速度更快 !
这与萨特关于最小化堆活动的不断讨论相冲突。 我非常尊重Sutter和Alexandrescou的见解,所以我对这两个关键问题感到有点困惑
-
不通过引用创build类实例会导致很多不必要的堆活动吗?
-
在哪些情况下我们可以使用垃圾收集而不牺牲运行时间性能?
要直接回答你的两个问题:
-
是的,通过引用创build类实例会导致大量的堆活动, 但是 :
一个。 在D中,你有
struct
以及class
。 一个struct
具有值语义,除了多态性之外,它可以完成一个类所能做的所有事情。湾 由于切片问题,多态性和价值语义从来没有一起合作过。
C。 在D中,如果你真的需要在一些性能关键的代码中在堆栈上分配一个类实例,并且不关心安全性的损失,那么你可以通过
scoped
函数没有不合理的麻烦。 -
在以下情况下,GC可以与手动内存pipe理相比或更快:
一个。 你仍然可以在堆栈上进行分配(就像你通常在D中做的那样),而不是依赖所有的堆(就像你经常用其他GC语言做的那样)。
湾 你有一个顶级的垃圾收集器(D的当前GC实现无疑是天真的,虽然它在过去的几个版本已经看到了一些主要的优化,所以它没有那么糟糕)。
C。 你主要分配小物件。 如果你主要分配大数组并且性能最终成为一个问题,那么你可能需要将其中的一部分切换到C堆(你可以访问C的malloc并且在D中是空闲的),或者如果它有一个作用域生命期,像RegionAllocator一样的分配器。 (RegionAllocator目前正在讨论和完善,最终纳入D标准库)。
d。 你不太关心太空效率。 如果GC太频繁地运行以使内存占用超低,性能将受到影响。
在堆上创build对象比在堆栈上创build对象要慢的原因是内存分配方法需要处理像堆碎片这样的事情。 在堆栈上分配内存就像增加堆栈指针(一个常量操作)一样简单。
然而,使用压缩垃圾回收器,您不必担心堆碎片,堆分配可以像堆栈分配一样快。 D编程语言的垃圾收集页面更详细地解释了这一点。
GC'd语言运行得更快的断言可能是假设许多程序在堆上分配内存的次数比堆栈上的多。 假设在GC语言中堆分配的速度可能会更快,那么你就可以优化大部分程序(堆分配)的一大部分。
答案1):
只要你的堆是连续的 ,分配就像在堆栈上分配一样便宜。
最重要的是,当你分配彼此相邻的对象时,你的内存caching性能会很好。
只要你不需要运行垃圾收集器, 性能就不会丢失 ,并且堆保持连续。
这是个好消息:)
回答2):
气相色谱技术有了很大进步 他们甚至在现在实时口味。 这意味着保证连续的内存是一个由策略驱动的,依赖于实现的问题。
因此,如果
- 你可以负担得起一个实时的gc
- 在您的应用程序中有足够的分配暂停
- 它可以让你的自由列表免费块
你可能会有更好的performance。
回答未经询问的问题:
如果开发人员从内存pipe理问题中解脱出来,他们可能会有更多时间花在代码中的真实性能和可伸缩性方面。 这也是一个非技术因素 。
这不是“垃圾收集”或“乏味容易出错”的手写代码。 智能指针,真正聪明的可以给你堆栈的语义,并意味着你从来没有input“删除”,但你不支付垃圾收集。 这是Herb的另一个video,这个video安全而且快速 – 这就是我们想要的。
还有一点要考虑的是80:20的规则。 你分配的绝大多数地方可能是不相关的,即使你可以将成本推到零,你也不会获得太多的GC。 如果您接受,那么通过使用GC可以获得的简单性可以取代使用它的成本。 如果你可以避免复制,尤其如此。 D所提供的是80%的情况下的GC,以及20%的堆栈分配和malloc访问。
即使你有理想的垃圾收集器,它仍然会比在堆栈上创build东西慢。 所以你必须有一个允许在同一时间的语言。 而且,实现与垃圾收集器相同的性能的唯一方法就是使用手动pipe理的内存分配(正确的方式),就是使内存与经验丰富的开发人员做相同的事情,而且在很多情况下需要一个垃圾收集器的决定在编译时作出并在运行时执行。 通常情况下,垃圾回收会让速度变慢,只有使用dynamic内存的语言会比较慢,而且用这些语言编写的程序执行的可预测性较低,而执行时间较长。 坦率地说,我个人不明白为什么需要垃圾收集器。 手动pipe理内存并不难。 至less不在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将你的堆对象压缩在一起时,改进的caching使用可能会提高性能。 这也是杰克·埃德蒙兹(Jack Edmonds)的解答中所解决的堆碎裂问题。 类似的事情可以通过手动内存pipe理来完成,但这是额外的工作。
高优先级任务不运行时,增量低优先级GC将收集垃圾。 高优先级线程运行得更快,因为不会执行内存重新分配。 这是Henriksson的RT Java GC的想法,请参阅http://www.oracle.com/technetwork/articles/javase/index-138577.html
垃圾收集实际上使代码变慢。 除了代码之外,它还为程序添加了额外的function。 还有其他一些问题,例如,直到实际需要内存时GC才运行。 这可能导致很小的内存泄漏。 另一个问题是,如果没有正确移除引用,GC将不会将其提取出来,并再次导致泄漏。 我与GC的另一个问题是它促进程序员的懒惰。 在进入更高层次之前,我主张学习低层次的内存pipe理概念。 这就像math。 你学会了如何解决一个二次方程的根,或者如何先手工取出一个导数,然后学习如何在计算器上完成。 使用这些东西作为工具,而不是拐杖。
如果你不想击中你的performance,那么要对GC和你的堆vs堆的使用情况做个聪明点。
我的观点是,当你进行正常的程序编程时,GC不如malloc。 你只要从程序到程序,分配和释放,使用全局variables,并声明一些函数_inline或_register。 这是C风格。
但是一旦你走到更高的抽象层,至less需要引用计数。 所以你可以通过引用传递,计数它们,并在计数器为零时自由。 这是好的,并且在对象的数量和层次变得难以手动pipe理之后,优于malloc。 这是C ++风格。 您将定义构造函数和析构函数来增加计数器,您将进行复制修改,因此共享对象将被拆分为两部分,一部分由一方修改,而另一方仍然需要原始值。 所以你可以将大量的数据从函数传递到函数,而不用考虑是否需要在这里复制数据或只是在那里发送一个指针。 重新计数为你做出这些决定。
然后是全新的世界,closures,函数式编程,鸭子打字,循环引用,asynchronous执行。 代码和数据开始混合,你发现自己作为parameter passing函数比正常数据更频繁。 您意识到,元编程可以在没有macros或模板的情况下完成。 你的代码开始沉浸在空中,失去了坚实的基础,因为你在callback的callback函数中执行某些东西,数据变得没有根据,事物变得asynchronous,你会沉迷于闭包variables。 所以这就是基于定时器的内存遍历GC是唯一可能的解决scheme,否则封闭和循环引用是不可能的。 这是JavaScript的方式。
你提到D,但是D仍然是改进的C ++,所以在构造函数,堆栈分配,全局variables(即使它们是各种实体的复合树)中malloc或ref计数可能是你select的。