我最近看到了两个非常好的和有教育意义的语言讲座:
This first one 由 Herb Sutter 撰写,介绍了 C++0x 的所有优秀而酷炫的特性,为什么 C++ 的未来似乎比以往任何时候都更加光明,以及 M$ 是如何被称为这个游戏中的好人的。讨论围绕着效率以及最小化堆活动如何经常提高性能。
This other one,作者 Andrei Alexandrescu,推动了从 C/C++ 到他的新游戏规则改变者 D 的过渡。 D 的大部分东西似乎都非常有动力和设计。然而,让我感到惊讶的是,D 推动了垃圾收集,并且所有类都是通过引用单独创建的。更令人困惑的是,The D Programming Language Ref Manual 一书特别在关于资源管理 的部分中陈述了以下内容,引用:
垃圾收集消除了 C 和 C++ 中所需的繁琐、容易出错的内存分配跟踪代码。这不仅意味着更快的开发时间和更低的维护成本,而且生成的程序通常运行得更快!
这与 Sutter 一直在谈论最小化堆活动相冲突。我非常尊重 Sutter 和 Alexandrescou 的见解,所以我对这两个关键问题感到有些困惑
仅通过引用创建类实例不会导致大量不必要的堆活动吗?在哪些情况下我们可以在不牺牲运行时性能的情况下使用垃圾收集?
直接回答你的两个问题:
是的,通过引用创建类实例确实会导致大量堆活动,但是:在 D 中,你有结构和类。结构具有值语义,并且可以做类可以做的所有事情,除了多态性。湾。由于切片问题,多态性和值语义从来没有很好地协同工作。 C。在 D 中,如果您确实需要在一些性能关键的代码中在堆栈上分配一个类实例并且不关心安全性的损失,您可以通过作用域函数这样做而不会造成不合理的麻烦。如果满足以下条件,GC 可以与手动内存管理相当或更快:您仍然尽可能在堆栈上分配(就像您通常在 D 中所做的那样),而不是依赖堆来处理所有事情(就像您在其他 GC'd 语言中经常做的那样)。湾。你有一个顶级的垃圾收集器(D 当前的 GC 实现确实有点幼稚,尽管它在过去的几个版本中看到了一些主要的优化,所以它没有以前那么糟糕)。 C。您分配的主要是小对象。如果您分配大部分大型数组并且性能最终成为问题,您可能希望将其中一些切换到 C 堆(您可以访问 C 的 malloc 并在 D 中释放),或者,如果它具有作用域生命周期,则其他一些像 RegionAllocator 一样的分配器。 (RegionAllocator 目前正在讨论和完善,以便最终包含在 D 的标准库中)。 d。你不太关心空间效率。如果你让 GC 运行过于频繁以保持内存占用超低,性能将会受到影响。
在堆上创建对象比在堆栈上创建对象慢的原因是内存分配方法需要处理诸如堆碎片之类的事情。在堆栈上分配内存就像递增堆栈指针(一个恒定时间操作)一样简单。
然而,使用压缩垃圾收集器,您不必担心堆碎片,堆分配可以与堆栈分配一样快。 D 编程语言的 Garbage Collection 页更详细地解释了这一点。
GC 语言运行速度更快的断言可能是假设许多程序在堆上分配内存的频率比在堆栈上分配的频率高得多。假设堆分配在 GC 语言中可能更快,那么您刚刚优化了大多数程序的很大一部分(堆分配)。
1)的答案:
只要你的堆是连续的,在它上分配就和在堆栈上分配一样便宜。
最重要的是,当您分配彼此相邻的对象时,您的内存缓存性能将非常好。
只要您不必运行垃圾收集器,就不会损失任何性能,并且堆保持连续。
这是个好消息:)
回答2):
GC技术有了很大进步;如今,它们甚至有实时风味。这意味着保证连续内存是一个策略驱动的、依赖于实现的问题。
因此,如果
你买得起实时 gc
您的应用程序中有足够的分配暂停
它可以让你的空闲列表成为空闲块
您最终可能会获得更好的性能。
回答未提出的问题:
如果开发人员从内存管理问题中解脱出来,他们可能有更多时间花在代码中的实际性能和可伸缩性方面。这也是一个非技术因素发挥作用。
n
变量是一条机器指令。清理为零。
它既不是“垃圾收集”也不是“容易出错”的手写代码。真正智能的智能指针可以为您提供堆栈语义,这意味着您永远不会键入“delete”,但您无需为垃圾收集付费。 another video by Herb 说明了这一点 - 安全且快速 - 这就是我们想要的。
mov
大得多(尤其是内联析构函数代码时)。如果指针在线程之间共享,那么您甚至可能需要额外的代码来确保计数递增/递减是原子的。
T*
和 scoped_ptr<T>
就非常容易使用,其中没有一个是引用计数的。
要考虑的另一点是 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
}
编译器所做的称为 escape analysis。
此外,D could in theory have a moving GC,这意味着当 GC 将堆对象压缩在一起时,通过改进缓存使用来提高潜在的性能。正如 Jack Edmonds 的回答中所解释的,它还可以对抗堆碎片。类似的事情可以通过手动内存管理来完成,但这是额外的工作。
Foo
的构造函数和 doStuff
都不会导致对 x
的引用或其中的任何内容被泄露,否则它不能进行这样的优化。在 D 中,编译器会知道如果两个函数都是 pure
,因为这保证不会访问可变模块或静态变量,但在大多数语言中,它不能不检查这些函数的主体(大多数编译器都赢了) 't do),因为在其中任何一个函数中,可能已经为全局变量或类变量分配了 x
内部的值(包括 x
本身)。
pure
,那么 D 编译器可以知道没有引用正在转义它,因为 pure
函数不能访问任何可变的静态或模块变量,但它仍然不会进行这种优化反正。理论上可以,但我不相信任何 D 编译器都会将类放在堆栈上作为优化。
当高优先级任务未运行时,增量低优先级 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++ 风格。您将定义构造函数和析构函数以递增计数器,您将在修改时复制,因此共享对象将一分为二,一旦其中一部分被一方修改,但另一方仍需要原始值。因此,您可以在函数之间传递大量数据,而无需考虑是否需要在此处复制数据或只是在此处发送指针。引用计数会为您做出这些决定。
然后是全新的世界,闭包、函数式编程、鸭子类型、循环引用、异步执行。代码和数据开始混合,您发现自己比普通数据更频繁地将函数作为参数传递。您意识到元编程可以在没有宏或模板的情况下完成。你的代码开始在天空中浸泡并失去坚实的基础,因为你在回调的回调中执行一些东西,数据变得无根,事情变得异步,你沉迷于闭包变量。所以这是基于定时器的内存遍历 GC 是唯一可能的解决方案,否则闭包和循环引用根本不可能。这是 JavaScript 方式。
您提到了 D,但 D 仍然是改进的 C++,因此您可能选择在构造函数、堆栈分配、全局变量(即使它们是各种实体的复杂树)中进行 malloc 或 ref 计数。