RAII与垃圾收集器

我最近在CppCon 2016上观看了Herb Sutter关于“Leak Free C ++ …”的畅谈,他谈到了如何使用智能指针来实现RAII(资源获取是初始化) – 概念以及它们如何解决大部分的内存泄漏问题。

现在我在想。 如果我严格遵循RAII规则,这似乎是一件好事,那么为什么这与C ++中的垃圾收集器有什么不同呢? 我知道,在RAII中,程序员完全控制了资源何时被释放,但是在任何情况下都有利于只有垃圾收集器? 会不会有效率? 我甚至听说有一个垃圾收集器可以更有效率,因为它可以一次释放更大的内存块,而不是释放整个代码中的小内存块。

如果我严格遵循RAII规则,这似乎是一件好事,那么为什么这与C ++中的垃圾收集器有什么不同呢?

虽然两者都处理分配,但他们完全不同的方式。 如果您正在尝试像Java这样的GC,那么会增加自己的开销,从资源释放过程中删除一些确定性,并处理循环引用。

您可以对特定情况实施GC,但具有不同的性能特征。 我在一个高性能/高吞吐量的服务器上实现了一次closures套接字连接的操作(只是调用套接字closuresAPI花费的时间太长而导致吞吐量性能下降)。 这涉及没有内存,但networking连接,并没有循环依赖处理。

我知道,在RAII中,程序员完全控制了资源何时被释放,但是在任何情况下都有利于只有垃圾收集器?

这个确定性是一个GC根本不允许的特性。 有时候希望能够知道某个点后,执行了一个清理操作(删除临时文件,closuresnetworking连接等)。

在这种情况下,GC不会削减它,这是在C#中的原因(例如)你有IDisposable接口。

我甚至听说有一个垃圾收集器可以更有效率,因为它可以一次释放更大的内存块,而不是释放整个代码中的小内存碎片。

可以…取决于实施。

垃圾收集解决了RAII无法解决的某些资源问题。 基本上,它归结为循环依赖关系,你没有预先确定周期。

这给了它两个好处。 首先,RAII无法解决某些types的问题。 根据我的经验,这很less见。

更大的一点是,它可以让程序员懒惰, 而不关心内存资源的生命周期和某些其他资源,你不介意延迟清理。 当你不必关心某些问题时,你可以更多地关心其他问题。 这可以让你专注于你想要关注的问题的部分。

缺点是没有RAII,pipe理你想要限制的生命周期的资源是很难的。 GC语言基本上可以使你的生命周期非常简单,或者要求你手动进行资源pipe理,就像在C中一样,手动地声明你已经完成了一个资源。 他们的对象生命周期系统与GC密切相关,对于大型复杂(无周期)系统的严格生命周期pipe理并不适用。

公平地说,C ++中的资源pipe理需要大量的工作才能在如此大规模的复杂(无周期)系统中正确执行。 C#和类似的语言只是使它更难以触摸,作为交换,它们使简单的情况变得容易。

大多数GC实现也强制非局部完整的类; 创build一般对象的连续缓冲区,或者将一般对象组合成一个更大的对象,这不是大多数GC实现变得容易的事情。 另一方面,C#允许您创build具有有限能力的值typesstruct 。 在目前的CPU架构时代,caching友好性是关键,缺乏本地化的GC力量是一个沉重的负担。 由于这些语言大部分都有一个字节码运行时间,所以理论上JIT环境可以将常用的数据放在一起,但是与C ++相比,由于频繁的caching未命中而导致统一的性能损失。

GC的最后一个问题是释放是不确定的,有时会导致性能问题。 现代气相色谱仪使这个问题比过去less得多。

请注意, RAII是一种编程习惯用法,而GC是一种内存pipe理技术。 所以我们正在比较苹果和橘子。

但是我们可以将RAII仅限于其内存pipe理方面,并将其与GC技术进行比较。

所谓的基于RAII的内存pipe理技术(实际上意味着引用计数 ,至less在考虑内存资源时忽略其他文件如文件)和真正的垃圾收集技术之间的主要区别在于处理循环引用 (对于循环图 ) 。

有了引用计数,你需要专门为他们编写代码(使用弱引用或其他东西)。

在许多有用的情况下(想到std::vector<std::map<std::string,int>> ),引用计数是隐式的(因为它只能是0或1),实际上被省略,但是构造器和析构函数(RAII必不可less)的行为就好像有一个引用计数位(实际上不存在)。 在std::shared_ptr有一个真正的引用计数器。 但是内存仍然是隐式 手动pipe理的 (在构造函数和析构函数中触发newdelete ),但是“隐式” delete (在析构函数中)给出了自动内存pipe理的错觉。 但是, newdelete呼叫仍然发生(并且花费时间)。

顺便说一句,GC的实现可能(并且经常)以某种特殊的方式处理循环,但是你把这个负担留给了GC(例如阅读Cheney的algorithm )。

一些GCalgorithm(特别是代代码复制垃圾回收器)不会为单个对象释放内存,而是在复制之后集体释放。 在实践中,Ocaml GC(或SBCL的)可能比真正的C ++ RAII编程风格(对于某些而不是所有types的algorithm)更快。

一些GC提供了终结 (主要用于pipe理非内存外部资源,如文件),但很less使用它(因为大多数值只消耗内存资源)。 缺点是定稿不提供任何时间保证。 实际上,使用最终化的程序正在使用它作为最后的手段(例如文件的closures应该或多或less明确地在最终化之外发生,并且也与它们一起发生)。

你仍然可以使用GC进行内存泄漏(至less在使用不当的时候也是如此),例如当一个值被保存在某个variables或者某个字段中,但是将来永远不会被使用。 他们不经常发生。

我build议阅读垃圾收集手册 。

在你的C ++代码中,你可以使用Boehm的GC或者Ravenbrook的MPS或者编写你自己的跟踪垃圾收集器 。 当然使用GC是一种折衷(有一些不便,例如非确定性,缺乏时间保证等)。

我不认为RAI​​I是所有情况下处理记忆的最终方式。 在很多情况下,在一个真正高效的GC实现(想到Ocaml或SBCL)中编写程序比在C ++ 17中使用花哨的RAII风格编码更简单(开发)和更快(执行)。 在其他情况下则不是。 因人而异。

举个例子,如果你用C ++ 17编写一个Scheme解释器,并且使用最好的RAII风格,你仍然需要在其中编写(或使用)一个显式的 GC(因为Scheme堆有循环)。 而且大多数certificate助手都是用GC语言编写的,通常是function性的,因为很好的原因(我唯一知道的用C ++编码的是精益 )。

顺便说一下,我有兴趣findScheme的C ++ 17实现(但对自己编写代码不太感兴趣),最好是具有一些multithreading能力。

RAII和GC以完全不同的方向解决问题。 尽pipe有些人会说,但他们完全不同。

两者都解决了资源pipe理困难的问题。 垃圾收集通过使得开发人员不需要重视pipe理这些资源来解决这个问题。 RAII通过使开发人员更容易关注其资源pipe理来解决这个问题。 任何说自己做同样事情的人都有卖东西的东西。

如果你看看最近的语言趋势,你会发现两种方法都被用在同一种语言中,坦率地说,你真的需要双方的拼图。 你看到很多使用垃圾收集的语言,所以你不必关注大多数的对象,这些语言也提供了RAII解决scheme(比如with运算符的Python),这是你真正想要关注的时代给他们。

  • C ++通过构造函数/析构函数提供RAII,通过shared_ptr提供GC(如果我可以说refcounting和GC在同一类解决scheme中,因为它们都是为了帮助您不需要关注生命周期)
  • Python通过一个refcounting系统和一个垃圾收集器提供了RAII和GC
  • C#通过IDisposable提供RAII,并通过分代垃圾收集器using GC

这些模式正在以各种语言出现。

垃圾收集器的问题之一是很难预测程序的性能。

有了RAII,你知道在确切的时间资源将超出范围,你将清除一些记忆,这将需要一些时间。 但是,如果您不是垃圾收集器设置的主人,则无法预测何时会进行清理。

例如:用GC清理一堆小物体可以更有效地完成,因为它可以释放大块,但是不会快速操作,并且很难预测何时会发生,并且由于“大块清理”它将会花费一些处理器时间,可能会影响你的程序性能。

大致说来。 RAII成语对于延迟抖动可能更好。 垃圾收集器可能会更好的系统的吞吐量

关于这两个术语之一是“有益的”还是“有效的”这个问题的主要部分,如果没有给出大量的背景和对这些术语的定义进行争论,就不能得到回答。

除此之外,您基本上可以感受到古代的“Java或C ++是更好的语言”的紧张吗? 在评论中的flamewar劈啪作响。 我不知道这个问题的“可接受的”答案是什么样的,我很好奇,最终看到它。

但关于一个可能重要的概念差异的一点还没有被指出:用RAII,你被绑定到调用析构函数的线程。 如果您的应用程序是单线程的(即使Herb Sutter声称免费午餐结束了 :今天的大多数软件仍然单线程的),那么单个内核可能忙于处理对象的清理与实际计划相关的时间更长

与此相反,垃圾收集器通常运行在自己的线程中,甚至是multithreading中,因此(在一定程度上)与其他部分的执行分离。

(注:一些答案已经试图指出具有不同特性的应用程序模式,提到了效率,性能,延迟和吞吐量 – 但是这个特定的点还没有被提到)

“高效”是一个非常广泛的术语,从发展的angular度来看,RAII通常效率低于GC,但是在性能方面,GC的效率通常低于RAII。 但是可以为这两种情况提供控制示例。 如果在托pipe语言中有非常明确的资源分配模式,处理genericsGC可能会相当麻烦,就像使用RAII的代码在没有理由的情况下使用shared_ptr时候,效率会非常低。

RAII一律处理任何可被描述为资源的事物。 dynamic分配就是这样一种资源,但它们决不是唯一的,而且可以说并不是最重要的。 文件,套接字,数据库连接,GUI反馈等都是可以用RAII确定性pipe理的事情。

GC只处理dynamic分配,解决了程序员在程序生命周期中担心分配对象总量的问题(他们只关心最高并发分配量拟合)

垃圾收集和RAII每个支持一个共同的构造,而另一个则不适合。

在垃圾收集系统中,代码可以有效地将对不可变对象(例如string)的引用作为其中包含的数据的代理; 传递这样的引用几乎与传递“哑”指针一样便宜,并且比为每个所有者制作单独的数据副本或尝试追踪共享数据副本的所有权更快。 此外,垃圾收集系统通过编写一个创build可变对象的类,根据需要填充它并提供访问器方法,可以很容易地创build不可变对象types,同时避免泄露引用,使其一旦构造函数饰面。 如果需要广泛复制对不可变对象的引用,但是对象本身不能复制,那么GC就会打败RAII。

另一方面,RAII在处理对象需要从外部实体获得独家服务的情况方面非常出色。 虽然许多GC系统允许对象定义“最终确定”方法,并在被发现被放弃时要求通知,并且此类方法有时可能设法释放不再需要的外部服务,但是它们很less可靠以至于不能提供令人满意的方式确保及时发布外部服务。 为了pipe理不可替代的外部资源,RAII击败了GC。

GC获胜的情况与RAII获胜的情况之间的关键区别在于GC擅长pipe理可根据需要释放的可replace内存,但在处理不可替代资源方面效果不佳。 RAII善于处理拥有明确所有权的对象,但却不善于处理无主的不可变数据持有者,除了所包含的数据之外,他们没有真正的身份。

因为GC和RAII都不能很好地处理所有情况,所以语言能够为他们提供良好的支持。 不幸的是,专注于一个的语言往往把另一个视为事后的想法。

RAII和垃圾收集是为了解决不同的问题。

当你使用RAII的时候,你在堆栈上留下一个对象,唯一的目的是在离开方法范围时清理你想pipe理的任何对象(套接字,内存,文件等等)。 这是为了exception安全 ,而不仅仅是垃圾收集,这就是为什么你得到closures套接字和释放互斥等的响应。 (好吧,除了我之外,没有人提到互斥体)。如果引发exception,堆栈展开自然会清理方法使用的资源。

垃圾回收是内存的程序化pipe理,尽pipe你可以“垃圾回收”其他稀缺资源。 明确地释放它们使99%的时间更有意义。 将RAII用于类似文件或套接字的唯一原因是希望在方法返回时使用资源。

垃圾收集还会处理堆分配的对象,例如,工厂构造对象的实例并将其返回。 在控制必须离开范围的情况下拥有持久对象是垃圾收集的吸引力。 但是你可以在工厂使用RAII,所以如果在你返回之前抛出一个exception,你就不会泄漏资源。