首页 文章

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

提问于
浏览
243

我不是问这个问题,因为垃圾收集的优点首先 . 我提出这个问题的主要原因是我知道Bjarne Stroustrup已经说C会在某个时间点有垃圾收集器 .

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

交叉链接:

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

16 回答

  • 3

    如果你想要自动垃圾收集,C有很好的商业和公共域垃圾收集器 . 对于适合垃圾收集的应用程序,C是一种优秀的垃圾收集语言,其性能与其他垃圾收集语言相当 . 有关C中自动垃圾收集的讨论,请参阅C编程语言(第3版) . 另见Hans-J . Boehm的C和C垃圾收集站点 . 此外,C支持编程技术,允许内存管理安全且隐式,无需垃圾收集器 .

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

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

    希望这可以帮助 .

  • 145

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

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

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

  • 3

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

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

    编辑 - 自动垃圾收集器不是一个可预测性能的问题 .
    不知道GC什么时候开始就像雇用一个嗜睡的航空公司飞行员,大部分时间他们都很棒 - 但是当你真的需要响应时!

  • 53

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

    • 对象的确定性生命周期(引用计数给你这个,但GC没有 . 虽然它可能不是那么大的交易) .

    • 如果在对象被垃圾收集时析构函数抛出会发生什么?大多数语言都忽略了这个异常,因为它确实没有阻止能够传输它,但这可能不是C的可接受的解决方案 .

    • 如何启用/禁用它?当然,它可能是一个编译时决定,但是为GC编写的代码是为非GC编写的代码将会非常不同并且可能不兼容 . 你如何调和这个?

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

  • 12

    因为这些天,C不再需要它了 .

    对于这些天写的代码(C17和官方语言Coding Guidelines)的情况如下:

    • 大多数与内存所有权相关的代码都在库中(特别是那些提供容器的代码) .

    • 大多数使用涉及内存所有权的代码都遵循RAII pattern,因此在构造和销毁时取消分配时进行分配,在退出分配内容的范围时发生 .

    • do not explicitly allocate or deallocate memory directly .

    • 原始指针do not own memory(如果你通过传递它们来泄漏它们 .

    • 如果你要传递内存中值序列的起始地址 - 你将使用span进行传递;不需要原始指针 .

    • 如果你真的需要一个拥有"pointer",你使用C'standard-library smart pointers - 它们不会泄漏,而且效率很高 . 或者,您可以使用"owner pointers"跨范围边界传递所有权 . 这些是不常见的,必须明确使用;它们允许对泄漏进行部分静态检查 .

    “哦,是吗?但是......那又怎样呢......

    ...如果我只是按照我们编写C的方式编写代码呢?“

    实际上,您可以忽略所有指导原则并编写泄漏的应用程序代码 - 它将一如既往地编译和运行(和泄漏) .

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

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

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

    • 你很少这样做(就代码中的位置而言,不一定是执行时间的分数)

    • 你只会故意这样做,而不是偶然

    • 这样做会在符合指南的代码库中脱颖而出

    • 无论如何,这是用另一种语言绕过GC的代码

    ......图书馆开发?“

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

    所以,底线:一般来说,没有动力收集垃圾,因为你们都要确保不要产生垃圾 . GC正在成为C的无问题 .

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

  • 133

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

  • 4

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

    像C-Style中的任意指针和确定性析构函数这样的东西不仅会降低GC实现的性能,还会破坏大量C -legacy代码的向后兼容性 .

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

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

  • 19

    所有的技术讨论都使概念过于复杂 .

    如果您自动将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代码将不起作用 .

  • 3

    Stroustrup在2013 Going Native Session 上对此做了一些很好的评论 .

    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中,你只会得到一个关于该对象将被破坏的建议"at some point in the future"(更新:这更糟糕的是.Java不承诺永远调用终结器 - 它可能永远不会被调用) . 这不是't good enough if Gadget holds an open file handle, or a connection to a database, or data which you have buffered for write to a database at a later point. We want the Gadget to be destroyed as soon as it'完成,以便尽快释放这些资源 . 你不知道你的程序已经完成了 .

    那么解决方案是什么?有几种方法 . 您将用于绝大多数对象的显而易见的方法是:

    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 相同 . 可预测,易于阅读,易于教学 . 一切都是'value' . 有时候 Value 很大,但 Value 观更容易教,因为你不能用指针(或参考)来获得远距离的动作 .

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

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

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

    正如Stroustrup在视频中的其他地方所说(释义):"Only a computer scientist would insist on copying an object, and then destroying the original. (audience laughs). Why not just move the object directly to the new location? This is what humans (not computer scientists) expect."

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

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

  • 0

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

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

  • 10

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

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

    垃圾收集的一般思想是它应该合理地尝试确保内存分配成功 . 不幸的是,'s essentially impossible to guarantee that any memory allocation will succeed, even if you do have a garbage collector in operation. This is true to some extent in any case, but particularly so in the case of C++, because it'(可能)不可能使用在收集周期中移动内存中对象的复制收集器(或任何类似的东西) .

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

    虽然有可能提出一些保证说(实质上)如果重复完全重复相同的分配模式,并且第一次成功,它将在后续迭代中继续成功,前提是分配的内存在迭代之间变得无法访问 . 那个's such a weak guarantee it'本来没用,但我看不出任何加强它的合理希望 .

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

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

    至少对我而言,这引发了一个关于投资回报的严肃问题 . 我们确切地确定了多少,但绝对相当多),对实现和代码的新限制提出了新的要求,而我们获得的回报很可能一点都没有?

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

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

  • 4

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

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

  • 32

    简短回答:我们不知道如何有效地进行垃圾收集(具有较小的时间和空间开销)并且始终正确(在所有可能的情况下) .

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

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

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

  • 15

    主要有两个原因:

    • 因为它不需要一个(恕我直言)

    • 因为它与RAII非常不相容,后者是C的基石

    C已经提供了手动内存管理,堆栈分配,RAII,容器,自动指针,智能指针......这应该足够了 . 垃圾收集器适用于懒惰的程序员,他们不想花5分钟考虑谁应该拥有哪些对象或何时应该释放资源 . 这不是我们用C做事的方式 .

  • 0

    在这里增加辩论 .

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

    1. Performance ?

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

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

    • Mark-And-Sweep类

    • 参考 - 计数种类

    Mark And Sweep 更快(对整体性能的影响较小)但它受到"freeze the world"综合症的影响:即当GC启动时,其他所有内容都会停止,直到GC进行清理 . 如果你想构建一个在几毫秒内回答的服务器......某些交易将达不到你的期望:)

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

    我看到一篇由Eiffel实施者撰写的论文,他们试图实现一个 Reference Counting 垃圾收集器,它具有与 Mark And Sweep 相似的全局性能而没有"Freeze The World"方面 . 它需要一个单独的GC线程(典型值) . 该算法有点令人恐惧(最后),但是本文很好地一次介绍了一个概念,并展示了算法从"simple"版本到完全版本的演变 . 推荐阅读,只要我能把手放回PDF文件......

    2. Resources Acquisition Is Initialization

    C++ 中,您将在对象中包装资源的所有权以确保它们被正确释放是一种常见的习惯用法 . 它有垃圾收集,但它在许多其他情况下也很有用:

    • 锁(多线程,文件句柄,......)

    • 连接(到数据库,另一台服务器......)

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

    • 只要你需要它就应该活着

    • 当你完成它应该被杀死

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

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

    • don 't use GC when stack allocation is sufficient: it'通常用于性能问题,但在我们的情况下,它确实有帮助,因为范围定义了生命周期

    • using 构造......但它是显式(弱)RAII,而在C RAII中是隐含的,因此用户不会无意中犯错(通过省略 using 关键字)

    3. Smart Pointers

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

    一个人不能错 .

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

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

    它毕竟是什么促进,但它不是一颗银弹 . 事实上, shared_ptr 的主要问题在于它模拟由 Reference Counting 实现的GC,但您需要自己实现循环检测... Urg

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

    4. What's the solution ?

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

    如果可能的话,

    • 更愿意在一个给定时间拥有一个所有者

    • 如果没有,请确保您的类图没有任何与所有权相关的循环,并使用 weak_ptr 的微妙应用来打破它们

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

  • 8

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

    Bjarne Stroustrup本人的一句话:

    我曾希望可以选择启用的垃圾收集器将成为C 0x的一部分,但是我有足够的技术问题只需详细说明这样的收集器如何与其他语言集成,如果提供 . 与基本上所有C 0x特征的情况一样,存在实验实现 .

    对该主题进行了很好的讨论here .

    General overview:

    C非常强大,几乎可以做任何事情 . 出于这个原因,它不会自动将很多东西推到你身上,这可能会影响性能 . 使用智能指针(包含带引用计数的指针的对象)可以很容易地实现垃圾收集,当引用计数达到0时自动删除它们 .

    C是由竞争对手构建的,没有垃圾收集 . 与C和其他人相比,效率是C必须抵挡批评的主要问题 .

    垃圾收集有两种类型......

    Explicit garbage collection:

    C 0x将通过使用shared_ptr创建的指针进行垃圾收集

    如果你想要它你可以使用它,如果你不想要它你不会被迫使用它 .

    如果您不想等待C 0x,您当前也可以使用boost:shared_ptr .

    Implicit garbage collection:

    它没有透明的垃圾收集 . 不过,它将成为未来C规范的焦点 .

    Why Tr1 doesn't have implicit garbage collection?

    C 0x的tr1应该有很多东西,Bjarne Stroustrup在之前的采访中表示tr1没有他想要的那么多 .

相关问题