为什么C ++没有垃圾收集器?

时间:2008-09-29 00:53:21

标签: c++ garbage-collection c++11

由于垃圾收集的优点,我不是在问这个问题。我提出这个问题的主要原因是我知道Bjarne Stroustrup已经说过C ++在某个时间点会有一个垃圾收集器。

话虽如此,为什么还没有添加?已经有一些垃圾收集器用于C ++。这只是那些“说起来容易做起来难”的事情吗?还是有其他原因没有添加(并且不会在C ++ 11中添加)?

交叉链接:

为了澄清,我理解为什么C ++在第一次创建时没有垃圾收集器的原因。我想知道为什么收藏家不能加入。

16 个答案:

答案 0 :(得分:149)

可能已经添加了隐式垃圾收集,但它只是没有进行剪切。可能由于不仅仅是实施并发症,而且还因为人们无法以足够快的速度达成普遍共识。

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没有他想要的那么多。

答案 1 :(得分:137)

在这里增加辩论。

垃圾收集存在已知问题,了解它们有助于理解C ++中没有垃圾收集的原因。

<强> 1。表现?

第一个抱怨往往是关于表现,但大多数人并没有真正意识到他们在谈论什么。如Martin Beckett所示,问题可能不是绩效本身,而是绩效的可预测性。

目前有2个GC系列已广泛部署:

  • Mark-And-Sweep kind
  • 参考计数类

Mark And Sweep速度更快(对整体性能的影响较小),但它会受到“冻结世界”综合症的影响:即,当GC启动时,其他所有内容都会停止,直到GC进行清理。如果您希望构建一个能在几毫秒内完成答案的服务器......某些交易将达不到您的期望:)

Reference Counting的问题不同:引用计数会增加开销,尤其是在多线程环境中,因为您需要具有原子计数。此外还存在参考周期的问题,因此您需要一个聪明的算法来检测这些周期并消除它们(通常通过“冻结世界”来实现,尽管不那么频繁)。一般来说,截至今天,这种情况(即使通常更敏感或更确切地说,冻结频率较慢)比Mark And Sweep慢。

我看过埃菲尔实施者的一篇论文,他们试图实现一个Reference Counting垃圾收集器,它与Mark And Sweep具有类似的全局性能而没有“冻结世界”方面。它需要一个单独的GC线程(典型值)。这个算法有点令人恐惧(最后),但是本文很好地一次介绍了一个概念,并展示了算法从“简单”版本到完整版本的演变。推荐阅读,只要我能把手放回PDF文件......

<强> 2。资源获取是初始化

C++中常见的习惯用法是将对象中的资源所有权包装起来以确保它们被正确释放。它主要用于内存,因为我们没有垃圾收集,但它在许多其他情况下也很有用:

  • 锁(多线程,文件句柄,......)
  • 连接(到数据库,另一台服务器......)

想法是正确控制对象的生命周期:

  • 只要你需要它就应该活着
  • 当你完成它时应该被杀死

GC的问题在于,如果它有助于前者并最终保证以后......这种“终极”可能还不够。如果你发布一个锁,你真的希望它现在被释放,这样它就不会阻止任何进一步的通话!

GC的语言有两种解决方法:

  • 当堆栈分配足够时不要使用GC:它通常用于性能问题,但在我们的情况下它确实有帮助,因为范围定义了生命周期
  • using构造......但它是显式(弱)RAII,而在C ++中RAII是隐式的,因此用户不会无意中犯错(通过省略using关键字)

第3。智能指针

智能指针通常在C++中显示为处理内存的银弹。我经常听说过:毕竟我们不需要GC,因为我们有智能指针。

一个人不能错。

智能指针确实有帮助:auto_ptrunique_ptr使用RAII概念,确实非常有用。它们非常简单,你可以很容易地自己编写它们。

当需要共享所有权时,它会变得更加困难:您可能在多个线程之间共享,并且处理计数存在一些微妙的问题。因此,人们自然会走向shared_ptr

这很好,毕竟这就是Boost,但它不是一颗银弹。实际上,shared_ptr的主要问题是它模拟了由Reference Counting实现的GC,但您需要自己实现循环检测... Urg

当然有这个weak_ptr的东西,但遗憾的是,尽管使用了shared_ptr因为这些循环,我已经看到了内存泄漏...当你处于多线程环境时,它是极难发现!

<强> 4。解决方案是什么?

没有银弹,但一如既往,这绝对是可行的。在没有GC的情况下,需要明确所有权:

  • 如果可能的话,最好在一个给定的时间拥有一个所有者
  • 如果没有,请确保您的类图没有任何与所有权相关的循环,并使用weak_ptr
  • 的微妙应用来打破它们

事实上,拥有GC会很棒......但这不是一个小问题。与此同时,我们只需要卷起袖子。

答案 2 :(得分:53)

什么类型?它应该针对嵌入式洗衣机控制器,手机,工作站还是超级计算机进行优化? 它应该优先考虑gui响应能力还是服务器负载? 它应该使用大量内存还是大量CPU?

C / c ++用于太多不同的情况。 我怀疑像智能指针这样的东西对大多数用户来说已经足够了

编辑 - 自动垃圾收集器不是性能问题(你可以随时购买更多服务器)这是一个可预测性能的问题。
不知道GC什么时候开始就像雇用一个嗜睡的航空公司飞行员一样,大部分时间他们都很棒 - 但是当你真的需要回应时!

答案 3 :(得分:33)

C ++没有内置垃圾收集的最大原因之一是让垃圾收集与析构函数一起运行真的很难。据我所知,没有人真正知道如何完全解决它。有很多问题要处理:

  • 对象的确定性生命周期(引用计数给你这个,但GC没有。虽然它可能不是那么大的交易)。
  • 当对象被垃圾收集时,如果析构函数抛出会发生什么?大多数语言都忽略了这个异常,因为它确实没有阻止能够传输它,但这可能不是C ++可接受的解决方案。
  • 如何启用/禁用它?当然,它可能是一个编译时决定,但是为GC编写的代码和为非GC编写的代码将会非常不同并且可能不兼容。你如何调和这个?

这些只是面临的一些问题。

答案 4 :(得分:19)

虽然这是一个旧的问题,但仍有一个问题我根本没有看到任何人解决过:垃圾收集几乎无法指定。

特别是,C ++标准非常谨慎地根据外部可观察行为来指定语言,而不是实现如何实现该行为。但是,在垃圾收集的情况下, 几乎没有外部可观察的行为。

垃圾收集的一般想法是它应该合理地尝试确保内存分配成功。不幸的是,即使你确实有垃圾收集器在运行,它也基本上不可能保证任何内存分配都会成功。在任何情况下都是如此,但在C ++的情况下尤其如此,因为它(可能)不可能使用在收集周期期间在内存中移动对象的复制收集器(或类似的东西)。

如果您无法移动对象,则无法创建单个连续的内存空间来进行分配 - 这意味着您的堆(或免费商店,或您喜欢调用的任何内容)它会随着时间的推移而变得支离破碎。反过来,这可以防止分配成功,即使没有比请求的数量更多的内存。

虽然有可能提出一些保证(实质上)说如果重复完全重复相同的分配模式,并且第一次成功,它将继续如果分配的内存在迭代之间变得不可访问,则在后续迭代中成功。这种弱势保证它本质上毫无用处,但我看不出任何加强它的合理希望。

即便如此,它比C ++提出的要强大。 previous proposal [警告:PDF](已被删除)并不保证任何事情。在28页的提案中,您对外部可观察行为的看法是单一(非规范性)说明:

  

[注意:对于垃圾收集程序,高质量的托管实现应该尝试最大化它回收的无法访问的内存量。 - 后注]

至少在我看来,这引发了关于投资回报的严重的问题。我们打算破坏现有的代码(没有人确切地确定了多少,但肯定相当多),对实现和代码的新限制提出了新的要求,我们得到的回报很可能是什么都没有所有?

即使充其量,我们得到的是基于testing with Java的程序,可能需要大约六倍的内存才能以与现在相同的速度运行。更糟糕的是,垃圾收集从一开始就是Java的一部分--C ++对垃圾收集器施加了更多的限制,它几乎肯定会有更差的更差成本/收益比(即使我们超越了建议保证并假设会有一些好处)。

我以数学方式总结了这种情况:这是一个复杂的情况。正如任何数学家所知,复数有两部分:真实的和虚构的。在我看来,我们这里所拥有的是真实的成本,但是(至少大部分)想象的好处。

答案 5 :(得分:15)

  

如果你想自动垃圾收集,有很好的商业性   和C ++的公共域垃圾收集器。适用于哪些应用   垃圾收集很合适,C ++是一个很好的垃圾收集   具有与其他垃圾相比有利的性能的语言   收集的语言。有关C ++中自动垃圾收集的讨论,请参阅The C++ Programming Language (4rd Edition)。   另见Hans-J。 Boehm的site for C and C++ garbage collectionarchive)。

     

此外,C ++支持允许内存的编程技术   管理是safe and implicit without a garbage collector。我认为垃圾收集是最后的选择,也是资源管理不完善的处理方式。这并不意味着它永远不会有用,只是在许多情况下有更好的方法。

来源:http://www.stroustrup.com/bs_faq.html#garbage-collection

至于为什么它没有内置,如果我没记错,它是在GC之前发明的的东西,而且我不相信该语言可能有GC有几个原因(IE向后兼容C)

希望这有帮助。

答案 6 :(得分:12)

Stroustrup在2013年的Going Native会议上对此做了一些很好的评论。

this video中跳过大约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中,你只是得到一个建议,即“在未来的某个时刻”对象将被破坏(更新:它更糟糕的是这个。 承诺永远调用终结器 - 它可能永远不会被调用)。如果Gadget持有一个打开的文件句柄,或者一个数据库连接,或者你为了稍后写入数据库而缓冲的数据,这就不够了。我们希望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的行为与intdouble相同。可预测,易于阅读,易于教学。一切都是'价值'。有时价值很高,但价值观更容易教,因为你没有用指针(或参考)得到的“远距离”动作。

您创建的大多数对象仅用于创建它们的函数,并且可能作为输入传递给子函数。程序员在返回对象时不必考虑“内存管理”,或者在软件的各个部分之间共享对象。

范围和寿命很重要。大多数情况下,如果寿命与范围相同则更容易。它更容易理解,更容易教学。当你想要一个不同的生命周期时,通过使用shared_ptr来阅读你正在执行此操作的代码应该是显而易见的。 (或者按值返回(大)对象,利用移动语义或unique_ptr

这似乎是一个效率问题。如果我想从foo()返回小工具该怎么办? C ++ 11的移动语义使得返回大对象变得更容易。只需编写Gadget foo() { ... },它就可以正常工作,并且可以快速完成工作。你不需要自己弄乱&&,只需按值返回东西,语言通常可以进行必要的优化。 (甚至在C ++ 03之前,编译器在避免不必要的复制方面做得非常好。)

正如Stroustrup在视频中的其他地方所说(释义):“只有计算机科学家会坚持复制一个物体,然后摧毁原件。(观众笑)。为什么不直接将物体移动到新的位置?这是人类(而不是计算机科学家)所期望的。“

当您可以保证只需要一个对象的副本时,就可以更容易地理解对象的生命周期。您可以选择所需的生命周期策略,如果需要,可以使用垃圾收集。但是当您了解其他方法的好处时,您会发现垃圾收集位于您的首选项列表的底部。

如果这对您不起作用,您可以使用unique_ptr,或者使用shared_ptr。在内存管理方面,编写得好的C ++ 11比其他许多语言更短,更容易阅读,也更容易教授。

答案 7 :(得分:10)

C ++背后的理念是,您不会为不使用的功能支付任何性能影响。因此,添加垃圾收集意味着让一些程序直接在硬件上以C的方式运行,某些程序在某种运行时虚拟机中运行。

没有什么能阻止您使用绑定到某些第三方垃圾收集机制的某种形式的智能指针。我似乎记得微软用COM做了类似的事情,并没有顺利进行。

答案 8 :(得分:8)

要回答有关C ++的大多数“为什么”的问题,请阅读Design and Evolution of C++

答案 9 :(得分:5)

因为现代C ++不需要垃圾收集。

Bjarne Stroustrup的常见问题answer on this matter says

  

我不喜欢垃圾。我不喜欢乱扔垃圾。我的理想是通过不产生任何垃圾来消除对垃圾收集器的需求。现在可以了。

对于这些天写的代码(C ++ 17和官方Core Guidelines之后的代码)的情况如下:

  • 大多数与内存所有权相关的代码都在库中(特别是那些提供容器的代码)。
  • 大多数使用涉及内存所有权的代码都遵循RAII pattern,因此在构建和撤销时取消分配时会进行分配,这在退出分配内容的范围时会发生。< / LI>
  • do not explicitly allocate or deallocate memory directly
  • 原始指针do not own memory(如果你遵循了这些指导原则),所以你不能通过传递它们来泄漏。
  • 如果您想知道如何在内存中传递值序列的起始地址 - 您将使用span进行此操作;没有原始指针。
  • 如果你真的需要一个拥有“指针”,你使用C ++'standard-library smart pointers - 它们不会泄漏,并且效率很高。或者,您可以使用"owner pointers"跨范围边界传递所有权。这些是不常见的,必须明确使用;并且它们允许对泄漏进行部分静态检查。

“哦,是吗?但是......

...如果我只是按照过去编写C ++的方式编写代码?“

实际上,你可以只是忽略所有指南并编写漏洞的应用程序代码 - 它会像往常一样编译和运行(和泄漏)。

但这不是“只是不要那样做”的情况,开发人员应该是善良的,并且要进行大量的自我控制;编写不符合规范的代码并不简单,编写速度也不快,也不是表现更好。逐渐地,写入也会变得更加困难,因为您将面对符合代码提供和期望的“阻抗不匹配”。

...如果我reintrepret_cast?还是做指针运算?还是其他这样的黑客?“

事实上,如果你把它放在心上,你就可以编写令人讨厌的代码,尽管你对指南很好。但是:

  1. 你很少这样做(就代码中的位置而言,不一定是执行时间的分数)
  2. 你只会故意这样做,而不是偶然。
  3. 这样做会在符合指南的代码库中脱颖而出。
  4. 无论如何,这是用另一种语言绕过GC的代码。
  5. ......图书馆开发?“

    如果您是C ++库开发人员,那么您确实编写了涉及原始指针的不安全代码,并且您需要仔细和负责任地编写代码 - 但这些代码是由专家编写的自包含代码(更重要的是,通过专家)。

    所以,就像Bjarne说的那样:一般来说,没有动力收集垃圾,因为你们都要确保不要产生垃圾。 GC正在成为C ++的一个问题。

    这并不是说当您想要使用自定义分配和取消分配策略时,GC对某些特定应用程序来说不是一个有趣的问题。对于那些您需要自定义分配和取消分配的人,而不是语言级别的GC。

答案 10 :(得分:4)

原始C语言背后的基本原则之一是内存由一系列字节组成,而代码只需关心这些字节在它们被使用的确切时刻的含义。 Modern C允许编译器施加额外的限制,但C包括 - 和C ++保留 - 将指针分解为字节序列的能力,将包含相同值的任何字节序列组合成指针,然后使用该指针访问早期的对象。

虽然在某些类型的应用程序中,这种能力可能是有用的 - 甚至是必不可少的 - 但是包含该能力的语言在支持任何有用和可靠的垃圾收集方面的能力将非常有限。如果编译器不知道用构成指针的位完成的所有事情,那么它将无法知道在宇宙中的某个地方是否存在足以重建指针的信息。由于该信息可能以计算机无法访问的方式存储,即使它知道它们(例如,构成指针的字节可能已经在屏幕上显示足够长的时间以供某人写它们落在一张纸上),计算机可能几乎不可能知道将来是否可能使用指针。

许多垃圾收集框架的一个有趣的怪癖是对象引用不是由其中包含的位模式定义的,而是由对象引用中保存的位与其他地方保存的其他信息之间的关系定义的。在C和C ++中,如果存储在指针中的位模式标识了一个对象,那么该位模式将标识该对象,直到该对象被明确销毁。在典型的GC系统中,对象可以在某个时刻由位模式0x1234ABCD表示,但是下一个GC循环可以用对0x4321BABE的引用替换对0x1234ABCD的所有引用,于是对象将由后一种模式表示。即使要显示与对象引用相关联的位模式,然后稍后从键盘读回它,也不会期望相同的位模式可用于标识同一对象(或任何对象)。

答案 11 :(得分:3)

所有技术性讨论都使概念过于复杂。

如果您自动将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的现代计算机上,32BIt系统只有一个程序正在运行时才出现这样的问题。

作为另一个示例,请考虑必须跟踪所有对象的完整集合。首先,您必须扫描通过根可到达的所有对象。第二步扫描步骤1中可见的所有对象。然后扫描等待的析构函数。然后再次转到所有页面并关闭所有不可见的对象。这意味着许多页面可能会多次被换出并返回。

所以我的简短回答是,由于触摸所有内存而导致的PAGE FAULTS数量导致程序中所有对象的完整GC不可行,因此程序员必须将GC视为辅助像脚本和数据库工作之类的东西,但通过手动内存管理做正常的事情。

另一个非常重要的原因当然是全局变量。为了让收集器知道全局变量指针在GC中,它需要特定的关键字,因此现有的C ++代码不起作用。

答案 12 :(得分:3)

简短的回答: 我们不知道如何有效地进行垃圾收集(时间和空间开销很小)并且一直都是正确的(在所有可能的情况下)。

长期回答: 就像C一样,C ++是一种系统语言;这意味着它在您编写系统代码时使用,例如操作系统。换句话说,C ++的设计与C一样,最好的性能作为主要目标。语言标准不会添加任何可能妨碍性能目标的功能。

这暂停了一个问题:为什么垃圾收集会影响性能?主要原因是,当涉及到实施时,我们[计算机科学家]不知道如何以最小的开销进行垃圾收集,对于所有情况。因此,C ++编译器和运行时系统不可能始终有效地执行垃圾收集。另一方面,C ++程序员应该知道他的设计/实现,并且他是决定如何最好地进行垃圾收集的最佳人选。

最后,如果控制(硬件,细节等)和性能(时间,空间,功率等)不是主要约束,那么C ++不是写入工具。其他语言可能更好,并提供更多[隐藏]运行时管理,并带来必要的开销。

答案 13 :(得分:3)

当您将C ++与Java进行比较时,您可以立即看到C ++在设计时并未设计隐式垃圾收集,而Java则是。

在C-Style和确定性析构函数中使用任意指针不仅会降低GC实现的性能,还会破坏大量C ++的后向兼容性 - 遗留代码。

除此之外,C ++是一种旨在作为独立可执行文件运行的语言,而不是具有复杂的运行时环境。

总而言之: 是的,可以将垃圾收集添加到C ++中,但为了保持连续性,最好不要这样做。这样做的成本将高于收益。

答案 14 :(得分:0)

主要有两个原因:

  1. 因为它不需要一个(恕我直言)
  2. 因为它与RAII非常不兼容,RAII是C ++的基石
  3. C ++已经提供了手动内存管理,堆栈分配,RAII,容器,自动指针,智能指针......这应该足够了。垃圾收集器适用于懒惰的程序员,他们不想花5分钟考虑谁应该拥有哪些对象或什么时候应该释放资源。这不是我们用C ++做事的方式。

答案 15 :(得分:0)

实施垃圾收集实际上是低级别到高级别的范式转换。

如果你看一下使用垃圾收集语言处理字符串的方式,你会发现它们只允许高级字符串操作函数,并且不允许对字符串进行二进制访问。简单地说,所有字符串函数首先检查指针以查看字符串的位置,即使您只是绘制一个字节。因此,如果您正在使用垃圾收集语言处理字符串中的每个字节的循环,则必须为每次迭代计算基本位置加偏移量,因为它无法知道字符串何时移动。然后你必须考虑堆,堆栈,线程等等。