我最近看到两种非常好的和教育性的语言对话:
Herb Sutter的This first one介绍了C ++ 0x的所有优秀和酷炫功能,为什么C ++的未来比以往任何时候都更加明亮,以及M $如何在这场游戏中成为一个好人。这个讨论围绕着效率以及如何最大限度地减少堆活动来提高性能。
Andrei Alexandrescu的This other one激发了从C / C ++到他的新游戏改变者 D 的过渡。 D的大部分内容似乎都非常有动力和设计。然而,有一点让我感到惊讶,即D推送垃圾收集,并且所有类都是通过引用创建的。更令人困惑的是, The D Programming Language Ref Manual 这本书专门在关于资源管理的部分中陈述如下,引用:
垃圾收集消除了繁琐,容易出错的内存分配跟踪代码 在C和C ++中是必需的。这不仅意味着更快的开发时间和更低的开发时间 维护成本,但生成的程序经常更快!
这与Sutter关于最小化堆活动的不断讨论相冲突。我非常尊重Sutter和Alexandrescou的见解,所以我对这两个关键问题感到有点困惑
不仅仅通过引用创建类实例会导致很多不必要的堆活动吗?
在哪些情况下我们可以在不牺牲运行时性能的情况下使用垃圾收集?
答案 0 :(得分:44)
直接回答你的两个问题:
是的,通过引用创建类实例会导致很多堆活动,但是:
一个。在D中,您有struct
以及class
。 struct
具有值语义,并且可以执行类可以执行的所有操作,但多态性除外。
湾由于slicing problem,多态性和价值语义一直没有很好地协同工作。
℃。在D中,如果你真的需要在一些性能关键代码中在堆栈上分配一个类实例而不关心安全性的损失,那么你可以通过scoped
函数毫无理由地做到这一点。
如果符合以下条件,GC可以与手动内存管理相媲美或更快:
一个。您仍然尽可能在堆栈上进行分配(正如您在D中所做的那样),而不是依靠堆来处理所有事情(正如您在其他GC语言中经常做的那样)。
湾你有一个顶级的垃圾收集器(D当前的GC实现虽然在过去的几个版本中已经看到了一些主要的优化,但它并没有那么糟糕)。
℃。你主要分配小对象。如果你主要分配大型数组并且性能最终成为一个问题,你可能想要将其中一些切换到C堆(你可以访问C的malloc并在D中释放),或者,如果它有一个范围的生命周期,其他一些像RegionAllocator这样的分配器。 (RegionAllocator目前正在讨论和完善,以便最终包含在D的标准库中)。
d。你对空间效率并不在乎。如果过于频繁地运行GC以使内存占用率保持在极低水平,性能将受到影响。
答案 1 :(得分:22)
在堆上创建对象的原因比在堆栈上创建对象要慢,因为内存分配方法需要处理堆碎片之类的事情。在堆栈上分配内存就像递增堆栈指针一样简单(恒定时间操作)。
然而,使用压缩垃圾收集器,您不必担心堆碎片,堆分配可以像堆栈分配一样快。 D编程语言的Garbage Collection页面更详细地解释了这一点。
GC语言运行得更快的断言可能假设许多程序在堆上分配内存的频率比在堆栈上分配更多。假设在GC语言中堆分配可能更快,那么你就可以优化大部分程序的大部分(堆分配)。
答案 2 :(得分:13)
答案1):
只要您的堆连续,分配它就像在堆栈上分配一样便宜。
最重要的是,当你分配彼此相邻的对象时,你的内存缓存性能会很好。
只要不必须运行垃圾收集器,就不会丢失性能,并且堆保持连续。
这是个好消息:)
回答2):
GC技术已大大提升;他们甚至现在都有实时的味道。这意味着保证连续内存是一个策略驱动的,依赖于实现的问题。
所以,如果
你最终可能会有更好的表现。
回答未提出的问题:
如果开发人员摆脱内存管理问题,他们可能有更多时间在代码中花费真正的性能和可伸缩性方面。这也是一个非技术因素。
答案 3 :(得分:4)
它既不是“垃圾收集”,也不是“繁琐易错”的手写代码。真正智能的智能指针可以为您提供堆栈语义,并且意味着您从不输入“删除”,但您不需要为垃圾收集付费。这里的another video by Herb表明了这一点 - 安全而快速 - 这就是我们想要的。
答案 4 :(得分:4)
要考虑的另一点是80:20规则。您分配的绝大多数地方可能都是无关紧要的,即使您可以将成本降低到零,您也不会在GC上获得太多收益。如果您接受,那么使用GC可以获得的简单性可以取代使用它的成本。如果您可以避免复制,则尤其如此。 D提供的是80%的情况下的GC,以及20%的堆栈分配和malloc的访问权。
答案 5 :(得分:3)
即使你有理想的垃圾收集器,它仍然比在堆栈上创建东西要慢。所以你必须有一种允许同时使用的语言。此外,与手动管理的内存分配(以正确的方式完成)实现垃圾收集器相同性能的唯一方法是使其与经验丰富的开发人员所做的内存相同,并且在许多情况下会要求垃圾收集器在编译时做出决定并在运行时执行。通常,垃圾收集使事情变得更慢,仅使用动态内存的语言更慢,并且用这些语言编写的程序的执行的可预测性低,而执行的延迟更高。坦率地说,我个人不明白为什么人们需要垃圾收集器。手动管理内存并不难。至少不是用C ++。当然,我不介意编译生成代码,像我一样为我清理所有事情,但目前看来这似乎不可能。
答案 6 :(得分: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
}
编译器的作用称为escape analysis。
此外,D could in theory have a moving GC,这意味着当GC将堆对象压缩在一起时,通过改进缓存使用可以提高性能。它还可以解决堆碎片问题,如Jack Edmonds的回答所述。类似的事情可以通过手动内存管理来完成,但这是额外的工作。
答案 7 :(得分:2)
增量低优先级GC将在高优先级任务未运行时收集垃圾。高优先级线程将运行得更快,因为不会进行内存释放。 这是Henriksson的RT Java GC的想法,请参阅http://www.oracle.com/technetwork/articles/javase/index-138577.html
答案 8 :(得分:1)
垃圾收集确实会降低代码速度。它为除了代码之外必须运行的程序添加了额外的功能。它还有其他问题,例如,GC直到实际需要内存才运行。这可能导致小内存泄漏。另一个问题是如果没有正确删除引用,GC将不会将其拾取,并再次导致泄漏。我对GC的另一个问题是它有助于促进程序员的懒惰。在进入更高级别之前,我主张学习低级别的内存管理概念。这就像数学。你学会了如何解决二次方的根,或者如何先用手拿出导数,然后你学习如何在计算器上做到。使用这些东西作为工具,而不是拐杖。
如果您不想达到性能,请熟悉GC以及堆与堆栈的使用情况。
答案 9 :(得分:0)
我的观点是,当你进行正常的程序编程时,GC不如malloc。您只需从过程转到过程,分配和释放,使用全局变量,并声明一些函数_inline或_register。这是C风格。
但是一旦你进入更高的抽象层,你至少需要引用计数。因此,您可以通过引用传递,计数它们并在计数器为零时释放。这是好的,并且在对象的数量和层次变得难以手动管理之后优于malloc。这是C ++风格。您将定义构造函数和析构函数以增加计数器,您将复制修改,因此共享对象将分为两部分,一旦其中一部分被一方修改,但另一方仍需要原始值。因此,您可以将大量数据从函数传递到函数,而无需考虑是否需要在此处复制数据或只是在那里发送指针。引用计数会为您做出决定。
然后是全新世界,闭包,函数式编程,鸭子打字,循环引用,异步执行。代码和数据开始混合,你发现自己比普通数据更频繁地传递函数作为参数。您意识到元编程可以在没有宏或模板的情况下完成。你的代码开始在空中浸泡并失去稳固的基础,因为你正在执行回调回调的回调,数据变得无根,事情变得异步,你沉迷于闭包变量。因此,这是基于计时器的内存行走GC是唯一可行的解决方案,否则根本不可能进行闭包和循环引用。这是JavaScript方式。
你提到了D,但是D仍然是改进的C ++,所以malloc或者在构造函数,堆栈分配,全局变量(即使它们是所有类型的实体的复杂树)中的计数可能是你选择的。