ChatGPT解决这个技术问题 Extra ChatGPT

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

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 的见解,所以我对这两个关键问题感到有些困惑

仅通过引用创建类实例不会导致大量不必要的堆活动吗?在哪些情况下我们可以在不牺牲运行时性能的情况下使用垃圾收集?

当然你的意思是“创建对象”:)
7 票赞成和 3 票接近。在我看来,这是一个很好的问题!随它去!
我不得不承认,我有点不喜欢那个说 M$ 是个好人的人 :)
这是一个宗教问题,因为简单的答案是“永远不要给猴子核武器”。在 98% 的情况下,内存管理应该是自动的(我只是拉了一个数字),对于其他 2%,仍然是 C++。
@c69 这是另外 2% 的问题。我们不允许提问吗?

C
Community

直接回答你的两个问题:

是的,通过引用创建类实例确实会导致大量堆活动,但是:在 D 中,你有结构和类。结构具有值语义,并且可以做类可以做的所有事情,除了多态性。湾。由于切片问题,多态性和值语义从来没有很好地协同工作。 C。在 D 中,如果您确实需要在一些性能关键的代码中在堆栈上分配一个类实例并且不关心安全性的损失,您可以通过作用域函数这样做而不会造成不合理的麻烦。如果满足以下条件,GC 可以与手动内存管理相当或更快:您仍然尽可能在堆栈上分配(就像您通常在 D 中所做的那样),而不是依赖堆来处理所有事情(就像您在其他 GC'd 语言中经常做的那样)。湾。你有一个顶级的垃圾收集器(D 当前的 GC 实现确实有点幼稚,尽管它在过去的几个版本中看到了一些主要的优化,所以它没有以前那么糟糕)。 C。您分配的主要是小对象。如果您分配大部分大型数组并且性能最终成为问题,您可能希望将其中一些切换到 C 堆(您可以访问 C 的 malloc 并在 D 中释放),或者,如果它具有作用域生命周期,则其他一些像 RegionAllocator 一样的分配器。 (RegionAllocator 目前正在讨论和完善,以便最终包含在 D 的标准库中)。 d。你不太关心空间效率。如果你让 GC 运行过于频繁以保持内存占用超低,性能将会受到影响。


J
Jack Edmonds

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

然而,使用压缩垃圾收集器,您不必担心堆碎片,堆分配可以与堆栈分配一样快。 D 编程语言的 Garbage Collection 页更详细地解释了这一点。

GC 语言运行速度更快的断言可能是假设许多程序在堆上分配内存的频率比在堆栈上分配的频率高得多。假设堆分配在 GC 语言中可能更快,那么您刚刚优化了大多数程序的很大一部分(堆分配)。


啊哈!很有意思。我会调查的。谢谢!
这一切都是为了将语言的默认行为拟合(优化)到平均用例,对吧?这并不意味着某些算法在 GC 关闭的情况下仍然表现更好,顺便说一下 D 支持。恕我直言,这种灵活的方法应该会让更多人满意。
GC 效率低下,错误地使用 malloc 也是如此。通过内存池之类的东西管理内存将比 GC 快得多。
@Pubby:GC 可以在内部使用内存池。他们更常见的是按代划分事物(这非常有效,因为大多数对象都是短暂的)。 GC 的真正问题是它倾向于使用比其他方法更多的内存,这会降低 CPU 缓存的局部性(从而降低速度)。
分配(但不是解除分配)对于压缩垃圾收集器来说非常快,但页面实际上并没有说 D 有一个压缩垃圾收集器,只是说“现代垃圾收集器”正在压缩。事实上,我确定我在 D 网站的某个地方读到它的 GC 是非压缩的,但我再也找不到参考了。
x
xtofl

1)的答案:

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

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

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

这是个好消息:)

回答2):

GC技术有了很大进步;如今,它们甚至有实时风味。这意味着保证连续内存是一个策略驱动的、依赖于实现的问题。

因此,如果

你买得起实时 gc

您的应用程序中有足够的分配暂停

它可以让你的空闲列表成为空闲块

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

回答未提出的问题:

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


那么,这完全取决于算法以正确的顺序分配和访问数据的方式吗?
@Nordloew:消除碎片会对有效需要的页面数量产生很小的影响。访问顺序有更大的影响,但不在此范围内。
“只要你的堆是连续的,在它上面分配就和在栈上分配一样便宜。”。 - 好吧,在 C++ 中在堆栈上分配 n 变量是一条机器指令。清理为零。
是的,但在 DI 中,仍然会在堆栈上创建本地对象。他们只是让编译器处理堆/堆栈之间的区别,就像在 C++ 中编译器处理哪些寄存器一样。
@MooingDuck:我也有这样的感觉,在大多数情况下,这样的决定可以由编译器而不是程序员做出。您对做这些事情的 D 方式有任何参考吗?
K
Kate Gregory

它既不是“垃圾收集”也不是“容易出错”的手写代码。真正智能的智能指针可以为您提供堆栈语义,这意味着您永远不会键入“delete”,但您无需为垃圾收集付费。 another video by Herb 说明了这一点 - 安全且快速 - 这就是我们想要的。


智能指针只是引用计数,而引用计数只是穷人的 GC。引用计数也有性能成本,因为所有计数都会增加/减少(根据 Boehm GC 的一些旧研究,人们引用计数通常比跟踪 GC 慢)并且不处理周期。
引用计数也可能导致相当大的代码膨胀:简单的指针赋值变成函数调用或内联。两者都比简单的 mov 大得多(尤其是内联析构函数代码时)。如果指针在线程之间共享,那么您甚至可能需要额外的代码来确保计数递增/递减是原子的。
@dsimcha:我已经以这种风格开发了一段时间,仅使用 T*scoped_ptr<T> 就非常容易使用,其中没有一个是引用计数的。
@dsmicha:大多数 C++ 代码根本不需要引用计数的共享指针……大多数情况下,一个简单的 unique_ptr 就足够了,不会造成任何开销。即使您必须使用 ref-counted 指针(大多数时候可以轻松避免,但有时,使用 shared_ptr 可以为您节省大量工作),大多数时候您仍然不需要那么多分配或副本。
B
BCS

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


佚名

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


这也是我目前的看法。感谢您的详尽回答。
“内存管理”并不难……没错——但我在学习它时犯了很多错误,并且看到许多合作开发者也这样做,而且我将来可能会犯新的错误。 “无内存管理”要容易得多:)
@xtofl:您不认为学习它不仅会给您带来困难,而且对事物的工作原理有很多有用的理解吗?例如,我有一个朋友拥有计算机科学博士学位,Java 程序,他甚至不知道(几乎)Java 中的所有内容都是动态分配的?而且他对堆栈上的指针和对象等一无所知。我非常感谢汇编程序、C 和 C++ 真正激励我学习计算机如何工作和跳出框框生活。
@Vlad:但他真的需要知道这些吗?
@GMan:我完全同意你的看法。拥有美国两所最受尊敬的 IT 大学之一的博士学位会让您认为您知道计算机是如何工作的,因此他报名参加了低级别的工作,而低级别的事情很重要并且失败了。如果你坚持你的领域 - 当然。不知道这些事情不仅可以,这就是现实。例如,我不知道计算机是如何从无到有“完成”的,如果你把我送到 100 年前,我将不得不打扫厕所来谋生...... :-(
m
mpartel

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

一个体面的编译器几乎肯定会在以下示例中使 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 的回答中所解释的,它还可以对抗堆碎片。类似的事情可以通过手动内存管理来完成,但这是额外的工作。


嗯。这将被我的工具标记为内存泄漏......你在那里写的是D还是C++?
@xtofl 让我们说带有 GC 的 C++。不过这个概念很笼统。
实际上,除非编译器知道 Foo 的构造函数和 doStuff 都不会导致对 x 的引用或其中的任何内容被泄露,否则它不能进行这样的优化。在 D 中,编译器会知道如果两个函数都是 pure,因为这保证不会访问可变模块或静态变量,但在大多数语言中,它不能不检查这些函数的主体(大多数编译器都赢了) 't do),因为在其中任何一个函数中,可能已经为全局变量或类变量分配了 x 内部的值(包括 x 本身)。
出于兴趣,哪些 C++ 编译器实际上进行了这种优化?
Do 使用 C 链接器,因此它所做的链接时优化与 C 或 C++ 通常所做的一样多。如果一个函数是 pure,那么 D 编译器可以知道没有引用正在转义它,因为 pure 函数不能访问任何可变的静态或模块变量,但它仍然不会进行这种优化反正。理论上可以,但我不相信任何 D 编译器都会将类放在堆栈上作为优化。
J
Jonas W

当高优先级任务未运行时,增量低优先级 GC 将收集垃圾。高优先级线程将运行得更快,因为不会进行内存释放。这是 Henriksson 的 RT Java GC 的想法,请参阅 http://www.oracle.com/technetwork/articles/javase/index-138577.html


M
MGZero

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

如果您不想影响性能,请注意 GC 和堆与堆栈的使用。


GC(至少某些类型的 GC)的要点之一是正确删除引用包括覆盖它。请注意,即使在非 GC 领域,如果不这样做也是一个错误(悬空指针)。
@BCS当然,这正是我所指的。我见过很多程序员只是让他们的指针悬空,但我在非 GC 语言中看到的少得多。你没有那个安全毯告诉你你可以对内存管理更放松一些,所以你往往会更加谨慎。这与我关于懒惰的观点密切相关!
e
exebook

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

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

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

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


关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅