我之所以问这个问题,首先是因为垃圾收集的优点。我问这个的主要原因是我知道 Bjarne Stroustrup 说过 C++ 将在某个时间点有一个垃圾收集器。
说了这么多,怎么还没加呢?已经有一些用于 C++ 的垃圾收集器。这只是那些“说起来容易做起来难”的事情之一吗?还是有其他原因没有添加(并且不会在 C++11 中添加)?
交叉链接:
澄清一下,我理解 C++ 在首次创建时没有垃圾收集器的原因。我想知道为什么无法添加收集器。
我之所以问这个问题,首先是因为垃圾收集的优点。我问这个的主要原因是我知道 Bjarne Stroustrup 说过 C++ 将在某个时间点有一个垃圾收集器。
说了这么多,怎么还没加呢?已经有一些用于 C++ 的垃圾收集器。这只是那些“说起来容易做起来难”的事情之一吗?还是有其他原因没有添加(并且不会在 C++11 中添加)?
交叉链接:
澄清一下,我理解 C++ 在首次创建时没有垃圾收集器的原因。我想知道为什么无法添加收集器。
可以添加隐式垃圾收集,但它没有成功。可能不仅是由于实施的复杂性,还因为人们无法足够快地达成普遍共识。
Bjarne Stroustrup 本人的一句话:
我曾希望可以选择启用的垃圾收集器将成为 C++0x 的一部分,但是有足够的技术问题,我只需要详细说明这种收集器如何与语言的其余部分集成即可,如果提供。与基本上所有 C++0x 功能的情况一样,存在实验性实现。
有一个很好的讨论主题here。
总体概述:
C++ 非常强大,几乎可以让你做任何事情。出于这个原因,它不会自动将许多可能影响性能的东西推给您。垃圾收集可以很容易地用智能指针(用引用计数包装指针的对象,当引用计数达到 0 时自动删除自己)实现。
C++ 在构建时考虑了没有垃圾收集的竞争对手。与 C 和其他语言相比,效率是 C++ 必须抵御批评的主要问题。
有两种类型的垃圾收集...
显式垃圾收集:
C++0x 通过 shared_ptr 创建的指针进行垃圾收集
如果你想要它,你可以使用它,如果你不想要它,你不会被迫使用它。
对于 C++0x 之前的版本, boost:shared_ptr 存在并用于相同目的。
隐式垃圾回收:
虽然它没有透明的垃圾收集。不过,它将成为未来 C++ 规范的焦点。
为什么 Tr1 没有隐式垃圾回收?
C++0x 的 tr1 应该有很多东西,Bjarne Stroustrup 在之前的采访中表示 tr1 没有他想要的那么多。
在这里加入辩论。
垃圾收集存在已知问题,理解它们有助于理解为什么 C++ 中没有。
1.性能?
第一个抱怨通常是关于性能的,但大多数人并没有真正意识到他们在说什么。如Martin Beckett
问题所示,可能不是性能本身,而是性能的可预测性。
目前有 2 个 GC 系列被广泛部署:
速度更快(对整体性能的Mark And Sweep
影响较小),但它遭受“冻结世界”综合症的困扰:即当 GC 启动时,其他一切都停止,直到 GC 完成清理。如果您希望构建一个可以在几毫秒内回答的服务器......有些事务将无法达到您的期望:)
的问题Reference Counting
不同:引用计数增加了开销,尤其是在多线程环境中,因为您需要原子计数。此外,还有参考周期的问题,因此您需要一个聪明的算法来检测这些周期并消除它们(通常也通过“冻结世界”来实现,尽管频率较低)。一般来说,截至今天,这种(尽管通常反应更快,或者更确切地说,冻结的频率更低)比Mark And Sweep
.
我看到了 Eiffel 实现者的一篇论文,该论文试图实现一个垃圾收集器,该垃圾收集器在没有“冻结世界”方面的情况下Reference Counting
具有类似的全局性能。Mark And Sweep
它需要一个单独的 GC 线程(典型)。算法有点吓人(最后),但这篇论文很好地一次介绍了一个概念,并展示了算法从“简单”版本到成熟版本的演变。推荐阅读,如果我能把我的手重新放在 PDF 文件上……
2. 资源获取即初始化(RAII)
这是一个常见的习惯用法,C++
因为您将资源的所有权包装在一个对象中以确保它们被正确释放。它主要用于内存,因为我们没有垃圾收集,但它在许多其他情况下也很有用:
这个想法是正确控制对象的生命周期:
GC 的问题在于,如果它对前者有所帮助并最终保证以后……这个“最终”可能还不够。如果你释放一个锁,你真的很想现在就释放它,这样它就不会阻塞任何进一步的调用!
带有 GC 的语言有两种解决方法:
using
构造...但它是显式(弱)RAII,而在 C++ 中 RAII 是隐式的,因此用户不能无意中犯错误(通过省略using
关键字)3. 智能指针
智能指针通常作为处理内存的灵丹妙药出现在C++
. 我经常听到:毕竟我们不需要 GC,因为我们有智能指针。
大错特错了。
智能指针确实有帮助:auto_ptr
并且unique_ptr
使用 RAII 概念,确实非常有用。它们非常简单,您可以很容易地自己编写它们。
但是,当需要共享所有权时,它会变得更加困难:您可能会在多个线程之间共享,并且在处理计数方面存在一些微妙的问题。所以,自然而然的走向shared_ptr
。
这很棒,毕竟这就是 Boost 的用途,但它不是灵丹妙药。事实上,主要问题shared_ptr
是它模拟了一个 GC 实现Reference Counting
但你需要自己实现循环检测...... Urg
当然有这个weak_ptr
东西,但不幸的是,尽管使用了这些循环,但我已经看到了内存泄漏shared_ptr
......当你处于多线程环境中时,很难检测到!
4. 解决办法是什么?
没有灵丹妙药,但一如既往,绝对可行。在没有 GC 的情况下,需要明确所有权:
weak_ptr
因此,确实,拥有 GC 会很棒……但这不是一个小问题。与此同时,我们只需要卷起袖子。
什么样的?是否应该针对嵌入式洗衣机控制器、手机、工作站或超级计算机进行优化?
它应该优先考虑gui响应还是服务器加载?
它应该使用大量内存还是大量 CPU?
C/c++ 用于太多不同的情况。我怀疑像 boost smart pointers 这样的东西对于大多数用户来说就足够了
编辑 - 自动垃圾收集器不是性能问题(您总是可以购买更多服务器),而是可预测性能的问题。
不知道 GC 什么时候开始工作就像雇用一个嗜睡症的航空公司飞行员,大多数时候他们都很棒——但是当你真的需要响应时!
C++ 没有内置垃圾收集的最大原因之一是让垃圾收集与析构函数很好地配合起来非常非常困难。据我所知,没有人真正知道如何完全解决它。有很多问题需要处理:
这些只是面临的一些问题。
虽然这是一个老问题,但我仍然没有看到有人解决过一个问题:几乎不可能指定垃圾收集。
特别是,C++ 标准非常小心地根据外部可观察行为来指定语言,而不是实现如何实现该行为。然而,在垃圾收集的情况下,几乎没有外部可观察的行为。
垃圾收集的一般思想是它应该做出合理的尝试来确保内存分配成功。不幸的是,基本上不可能保证任何内存分配都会成功,即使您确实有一个垃圾收集器在运行。在任何情况下这在某种程度上都是正确的,但在 C++ 的情况下尤其如此,因为(可能)不可能使用复制收集器(或任何类似的东西)在收集周期中移动内存中的对象。
如果你不能移动对象,你就不能创建一个单一的、连续的内存空间来进行分配——这意味着你的堆(或自由存储,或者你喜欢叫它的任何东西)可以并且可能会,随着时间的推移变得支离破碎。反过来,这会阻止分配成功,即使可用内存多于请求的内存量也是如此。
虽然可能会提出一些保证(本质上),如果您重复完全相同的分配模式,并且第一次成功,它将在后续迭代中继续成功,前提是分配的内存在迭代之间变得无法访问。这是一个如此薄弱的保证,它基本上是无用的,但我看不到任何加强它的合理希望。
即便如此,它还是比为 C++ 提出的要强。先前的提案[警告:PDF](已被删除)根本不保证任何事情。在 28 页的提案中,阻碍外部可观察行为的是一个单一的(非规范性的)注释,上面写着:
[注意:对于垃圾收集程序,高质量的托管实现应该尝试最大化它回收的不可达内存量。——尾注]
至少对我来说,这引发了一个关于投资回报的严肃问题。我们要破坏现有的代码(没有人知道具体多少,但肯定不少),对实现提出新的要求,对代码提出新的限制,而我们得到的回报很可能什么都没有?
即使充其量,我们得到的是基于Java 测试的程序,它们可能需要大约六倍的内存才能以与现在相同的速度运行。更糟糕的是,垃圾收集从一开始就是 Java 的一部分——C++ 对垃圾收集器施加了足够多的限制,几乎可以肯定它的成本/收益比会更差(即使我们超出了提案所保证的范围并假设会有一些好处)。
我会以数学方式总结情况:这是一个复杂的情况。任何数学家都知道,复数有两部分:实数和虚数。在我看来,我们这里的成本是真实的,但收益(至少大部分)是想象的。
如果你想要自动垃圾收集,有很好的 C++ 商业和公共领域的垃圾收集器。对于适合垃圾收集的应用程序,C++ 是一种出色的垃圾收集语言,其性能可与其他垃圾收集语言相媲美。有关 C++ 中自动垃圾收集的讨论,请参阅The C++ Programming Language (4rd Edition)。另见,汉斯-J。Boehm 的C 和 C++ 垃圾收集站点(存档)。
此外,C++ 支持允许内存管理安全且隐式的编程技术,而无需垃圾收集器。我认为垃圾收集是最后的选择,也是处理资源管理的一种不完美的方式。这并不意味着它永远不会有用,只是在许多情况下都有更好的方法。
来源: http: //www.stroustrup.com/bs_faq.html#garbage-collection
至于为什么它没有内置它,如果我没记错的话,它是在 GC 出现之前发明的,而且我不相信该语言可能有 GC 有几个原因(IE 与 C 的向后兼容性)
希望这可以帮助。
Stroustrup 在 2013 年 Going Native 会议上对此做了一些很好的评论。
只需跳到此视频中的 25 分 50 秒左右。(我建议实际上观看整个视频,但这会跳到有关垃圾收集的内容。)
当您拥有一种非常棒的语言,可以轻松(并且安全、可预测、易于阅读和易于教授)以直接方式处理对象和值时,避免(显式)使用堆,那么你甚至不需要垃圾收集。
对于现代 C++,以及我们在 C++11 中拥有的东西,垃圾收集不再是可取的,除非在有限的情况下。事实上,即使在主要的 C++ 编译器之一中内置了一个好的垃圾收集器,我认为它也不会经常使用。避免 GC会更容易,而不是更难。
他展示了这个例子:
void f(int n, int x) {
Gadget *p = new Gadget{n};
if(x<100) throw SomeException{};
if(x<200) return;
delete p;
}
这在 C++ 中是不安全的。但它在 Java 中也是不安全的!在 C++ 中,如果函数提前返回,delete
则永远不会调用 。但是如果你有完整的垃圾收集,比如在 Java 中,你只会得到一个建议,即对象将在“将来的某个时候”被破坏(更新:更糟糕的是,Java没有承诺永远调用终结器——它可能永远不会被调用)。如果 Gadget 拥有打开的文件句柄、与数据库的连接或您为稍后写入数据库而缓冲的数据,这还不够好。我们希望小工具在完成后立即销毁,以便尽快释放这些资源。您不希望您的数据库服务器为成千上万不再需要的数据库连接而苦苦挣扎——它不知道您的程序已经完成工作。
那么解决方案是什么?有几种方法。您将对绝大多数对象使用的显而易见的方法是:
void f(int n, int x) {
Gadget p = {n}; // Just leave it on the stack (where it belongs!)
if(x<100) throw SomeException{};
if(x<200) return;
}
这需要更少的字符来键入。它没有new
妨碍。它不需要您输入Gadget
两次。对象在函数结束时被销毁。如果这是您想要的,这是非常直观的。 Gadget
s 的行为与int
or相同double
。可预测、易读、易教。一切都是“价值”。有时价值很大,但价值更容易教授,因为您没有通过指针(或引用)获得的这种“远距离行动”的东西。
您创建的大多数对象仅在创建它们的函数中使用,并且可能作为输入传递给子函数。程序员在返回对象时不必考虑“内存管理”,或者在软件的广泛分离的部分之间共享对象。
范围和生命周期很重要。大多数情况下,如果生命周期与范围相同,则更容易。它更容易理解,也更容易教授。当您想要一个不同的生命周期时,阅读您正在执行此操作的代码应该是显而易见的shared_ptr
,例如使用。(或按值返回(大)对象,利用移动语义或unique_ptr
.
这似乎是一个效率问题。如果我想从中退回小工具foo()
怎么办?C++11 的移动语义使得返回大对象变得更容易。只需编写Gadget foo() { ... }
它,它就会工作,并且工作得很快。你不需要自找麻烦&&
,只需按值返回,语言通常能够进行必要的优化。(甚至在 C++03 之前,编译器在避免不必要的复制方面做得非常好。)
正如 Stroustrup 在视频其他地方所说(释义):“只有计算机科学家才会坚持复制一个对象,然后破坏原件。(观众笑)。为什么不直接将对象移动到新位置?这就是人类(不是计算机科学家)期待。”
当您可以保证只需要一个对象的副本时,就更容易理解对象的生命周期。你可以选择你想要的生命周期策略,如果你愿意,垃圾收集就在那里。但是当您了解其他方法的好处时,您会发现垃圾收集在您的偏好列表的底部。
如果这对您不起作用,您可以使用unique_ptr
,或失败,shared_ptr
。在内存管理方面,与许多其他语言相比,编写良好的 C++11 更短、更易于阅读和更易于教学。
Bjarne Stroustrup关于这个问题的常见问题解答说:
我不喜欢垃圾。我不喜欢乱扔垃圾。我的理想是通过不产生任何垃圾来消除对垃圾收集器的需求。现在这是可能的。
对于这些天编写的代码(C++17 并遵循官方 核心指南),情况如下:
实际上,您可以忽略所有指南并编写泄漏的应用程序代码 - 它会像往常一样编译和运行(和泄漏)。
但这不是“只是不这样做”的情况,在这种情况下,开发人员应该是有德行的,并且会进行大量的自我控制;编写不符合标准的代码并不简单,编写起来也不是更快,性能也不是更好。逐渐地,它也将变得更加难以编写,因为您将面临越来越多的“阻抗不匹配”与符合代码提供和期望的内容。
reintrepret_cast
?还是做复杂的指针运算?还是其他类似的黑客?”事实上,如果你下定决心,你可以编写出把事情搞得一团糟的代码,尽管你可以很好地遵守这些准则。但:
如果您是 C++ 库开发人员,那么您确实会编写涉及原始指针的不安全代码,并且您需要仔细且负责任地编写代码 - 但这些是由专家编写的自包含代码(更重要的是,由专家审查)。
所以,就像 Bjarne 说的:一般来说,确实没有收集垃圾的动机,因为你们都确保不产生垃圾。GC 正在成为 C++ 的一个非问题。
这并不是说 GC 对于某些特定应用程序来说不是一个有趣的问题,当您想要使用自定义分配和取消分配策略时。对于那些你想要自定义分配和取消分配的人,而不是语言级别的 GC。
C++ 背后的想法是,您不会为不使用的功能支付任何性能影响。因此,添加垃圾收集意味着让一些程序像 C 那样直接在硬件上运行,而另一些程序则在某种运行时虚拟机中运行。
没有什么可以阻止您使用某种形式的智能指针,这些指针绑定到某些第三方垃圾回收机制。我似乎记得微软在 COM 上做过类似的事情,但效果并不好。
要回答有关 C++ 的大多数“为什么”问题,请阅读 C++的设计和演变
原始 C 语言背后的基本原则之一是内存由一系列字节组成,代码只需要关心这些字节在使用它们的确切时刻意味着什么。现代 C 允许编译器施加额外的限制,但 C 包括 - 并且 C++ 保留 - 将指针分解为字节序列,将包含相同值的任何字节序列组装成指针,然后使用该指针访问较早的对象。
虽然这种能力在某些类型的应用程序中可能是有用的——甚至是必不可少的——但包含这种能力的语言在支持任何有用和可靠的垃圾收集的能力方面将非常有限。如果编译器不知道对构成指针的位所做的一切,它就无法知道足以重建指针的信息是否存在于宇宙的某个地方。因为这些信息有可能以计算机无法访问的方式存储,即使它知道它们(例如,构成指针的字节可能已经在屏幕上显示足够长的时间供某人写入它们写在一张纸上),计算机实际上不可能知道将来是否可以使用指针。
许多垃圾收集框架的一个有趣的怪癖是对象引用不是由其中包含的位模式定义的,而是由对象引用中保存的位与其他地方保存的其他信息之间的关系定义的。在 C 和 C++ 中,如果存储在指针中的位模式标识了一个对象,则该位模式将标识该对象,直到该对象被显式销毁。在典型的 GC 系统中,一个对象可能在某个时刻由位模式 0x1234ABCD 表示,但下一个 GC 周期可能会将对 0x1234ABCD 的所有引用替换为对 0x4321BABE 的引用,因此该对象将由后一种模式表示。即使要显示与对象引用关联的位模式,然后再从键盘读回,
所有的技术讨论都使这个概念过于复杂。
如果您将所有内存的 GC 自动放入 C++ 中,那么请考虑使用 Web 浏览器之类的东西。Web 浏览器必须加载完整的 Web 文档并运行 Web 脚本。您可以将 Web 脚本变量存储在文档树中。在浏览器中打开许多选项卡的 BIG 文档中,这意味着每次 GC 必须执行完整收集时,它还必须扫描所有文档元素。
在大多数计算机上,这意味着会发生 PAGE FAULTS。因此,回答问题的主要原因是会发生PAGE FAULTS。当您的 PC 开始进行大量磁盘访问时,您就会知道这一点。这是因为 GC 必须接触大量内存才能证明无效指针。当您有一个真正的应用程序使用大量内存时,由于 PAGE FAULTS,必须扫描每个集合的所有对象是严重的。页面错误是指虚拟内存需要从磁盘读回 RAM。
所以正确的解决方案是将一个应用程序分成需要GC的部分和不需要GC的部分。在上面的 Web 浏览器示例中,如果文档树是使用 malloc 分配的,但 javascript 使用 GC 运行,那么每次 GC 启动时,它只会扫描一小部分内存和内存的所有 PAGED OUT 元素文档树不需要重新分页。
要进一步了解这个问题,请查看虚拟内存以及它是如何在计算机中实现的。这完全是因为当没有那么多 RAM 时,程序可以使用 2GB。在具有 2GB RAM 的 32 位系统的现代计算机上,只要一个程序正在运行,就不是这样的问题。
作为另一个示例,考虑一个必须跟踪所有对象的完整集合。首先,您必须扫描所有可通过根访问的对象。第二次扫描步骤 1 中可见的所有对象。然后扫描等待的析构函数。然后再次转到所有页面并关闭所有不可见的对象。这意味着许多页面可能会被多次换出和换回。
所以我简短的回答是,由于接触所有内存而发生的 PAGE FAULTS 的数量会导致程序中所有对象的完全 GC 不可行,因此程序员必须将 GC 视为脚本之类的辅助工具和数据库工作,但通过手动内存管理做正常的事情。
另一个非常重要的原因当然是全局变量。为了让收集器知道全局变量指针在 GC 中,它需要特定的关键字,因此现有的 C++ 代码将无法工作。
简短的回答:我们不知道如何有效地进行垃圾收集(时间和空间开销很小)并且始终正确(在所有可能的情况下)。
长答案:就像 C 一样,C++ 是一种系统语言。这意味着它在您编写系统代码(例如操作系统)时使用。换句话说,C++ 的设计就像 C 一样,以尽可能最佳的性能作为主要目标。该语言的标准不会添加任何可能阻碍性能目标的功能。
这暂停了一个问题:为什么垃圾收集会阻碍性能?主要原因是,在实现方面,我们 [计算机科学家] 不知道如何在所有情况下以最小的开销进行垃圾收集。因此,C++ 编译器和运行时系统不可能一直有效地执行垃圾收集。另一方面,C++ 程序员应该了解他的设计/实现,并且他是决定如何最好地进行垃圾收集的最佳人选。
最后,如果控制(硬件、细节等)和性能(时间、空间、功率等)不是主要限制因素,那么 C++ 就不是编写工具。其他语言可能会提供更好的服务并提供更多 [隐藏] 运行时管理,但会产生必要的开销。
当我们将 C++ 与 Java 进行比较时,我们发现 C++ 在设计时并没有考虑到隐式垃圾收集,而 Java 是。
在 C 风格中拥有诸如任意指针之类的东西不仅不利于 GC 实现,而且还会破坏大量 C++ 遗留代码的向后兼容性。
除此之外,C++ 是一种旨在作为独立可执行文件运行的语言,而不是具有复杂的运行时环境。
总而言之:是的,可以将垃圾收集添加到 C++,但为了连续性,最好不要这样做。
主要有两个原因:
C++ 已经提供了手动内存管理、堆栈分配、RAII、容器、自动指针、智能指针……这应该足够了。垃圾收集器适用于不想花 5 分钟思考谁应该拥有哪些对象或何时应该释放资源的懒惰程序员。这不是我们在 C++ 中做事的方式。
实施垃圾收集实际上是从低级到高级的范式转变。
如果您查看带有垃圾收集的语言中处理字符串的方式,您会发现它们只允许高级字符串操作函数并且不允许对字符串进行二进制访问。简而言之,所有字符串函数首先检查指针以查看字符串的位置,即使您只是绘制一个字节。因此,如果您正在执行一个循环来处理带有垃圾收集的语言中的字符串中的每个字节,它必须计算每次迭代的基本位置和偏移量,因为它不知道字符串何时移动。然后你必须考虑堆、堆栈、线程等。