RAII与垃圾收集器

时间:2017-06-02 09:12:46

标签: c++ memory-leaks garbage-collection smart-pointers

我最近看过Herb Sutter关于“泄漏免费C ++ ......”的精彩演讲,他在2016年CppCon上谈到了使用智能指针实现RAII(资源获取是初始化) - 概念以及它们如何解决大部分内存泄漏问题。

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

12 个答案:

答案 0 :(得分:61)

  

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

虽然两者都涉及分配,但它们以完全不同的方式进行。如果您正在考虑使用Java中的GC,那会增加自己的开销,从资源发布过程中删除一些确定性并处理循环引用。

您可以针对特定情况实施GC,具有不同的性能特征。我在高性能/高吞吐量服务器中实现了一次关闭套接字连接(仅调用套接字关闭API花了太长时间并且提高了吞吐量性能)。这不涉及内存,而是网络连接,也没有循环依赖性处理。

  

我知道使用RAII,程序员可以完全控制何时再次释放资源,但是在任何情况下都只对垃圾收集器有益吗?

这种决定论是GC根本不允许的功能。有时您希望能够知道在某一点之后,已执行清理操作(删除临时文件,关闭网络连接等)。

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

  

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

可以......取决于实施。

答案 1 :(得分:38)

垃圾收集解决了RAII无法解决的某些类别的资源问题。基本上,它归结为循环依赖关系,在此之前您不能识别循环。

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

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

缺点是如果没有RAII,管理您希望受限制的资源很难。 GC语言基本上可以将您简化为具有极其简单的范围限制生命周期,或者要求您手动执行资源管理(如C语言),并手动说明您已完成资源。它们的物体寿命系统与GC密切相关,并且对于大型复杂(无循环)系统的严格生命周期管理不起作用。

公平地说,C ++中的资源管理需要大量工作才能在如此庞大的复杂(无循环)系统中正常运行。 C#和类似语言只是让它变得更加难以接受,作为交换,它们使简单的案例变得容易。

大多数GC实施也会强制非本地化的完全成熟的类;创建一般对象的连续缓冲区,或将一般对象组合成一个更大的对象,并不是大多数GC实现变得容易的事情。另一方面,C#允许您创建具有有限功能的值类型struct。在当前的CPU架构时代,缓存友好性是关键,而GC部队缺乏局部性是一个沉重的负担。由于这些语言在大多数情况下都具有字节码运行时,理论上JIT环境可以将常用数据移动到一起,但是与C ++相比,由于频繁的缓存未命中,您通常会获得统一的性能损失。

GC的最后一个问题是解除分配是不确定的,有时会导致性能问题。与过去相比,现代地理信息系统使问题变得不那么重要了。

答案 2 :(得分:14)

请注意RAII是一种编程习惯,而GC是一种内存管理技术。所以我们将苹果与橙子进行比较。

但我们可以将RAII限制为仅的内存管理方面,并将其与GC技术进行比较。

所谓的基于RAII的内存管理技术(实际上意味着reference counting,至少在您考虑内存资源并忽略其他文件如文件时)和真正的garbage collection技术之间的主要区别在于处理循环引用(适用于cyclic graphs)。

通过引用计数,您需要专门为它们编码(使用weak references或其他内容)。

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

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

某些GC算法(特别是分代复制垃圾收集器)不会为单个对象释放内存,它会在复制后释放 en masse 。在实践中,Ocaml GC(或SBCL)可以比真正的C ++ RAII编程风格更快(对于某些,而不是所有类型的算法)。

某些GC提供finalization(主要用于管理非内存外部资源,如文件),但您很少使用它(因为大多数值仅消耗内存资源)。缺点是最终确定不提供任何时间保证。实际上,使用finalization的程序使用它作为最后的手段(例如,文件的关闭仍应在最终确定之外或多或少地明确地发生,并且也与它们一起)。

您仍然可以使用GC(以及RAII,至少在使用不当时)内存泄漏,例如:当某个值保存在某个变量或某个字段中但将来永远不会使用时。它们的发生频率较低。

我建议您阅读garbage collection handbook

在您的C ++代码中,您可以使用Boehm's GCRavenbrook's MPS或编写自己的tracing garbage collector代码。当然使用GC是一种权衡(存在一些不便,例如非确定性,缺乏时间保证等等。)

我不认为RAI​​I是在所有情况下处理记忆的最终方式。在一些情况下,在真正有效的GC实现中编写程序(想想Ocaml或SBCL)可以比在C ++ 17中使用花哨的RAII样式编码更简单(开发)和更快(执行)。在其他情况下,它不是。 YMMV。

例如,如果您使用最高级的RAII样式在C ++ 17中编写Scheme解释器,您仍然需要在其中编码(或使用)显式 GC(因为一个Scheme)堆有圆形)。大多数proof assistants都是用GC编辑的语言编写的,通常是函数式语言(我知道唯一一个用C ++编写的语言是Lean),原因很充分。

BTW,我有兴趣找到这样一个Scheme的C ++ 17实现(但对自己编码不太感兴趣),最好有一些多线程能力。

答案 3 :(得分:13)

RAII和GC在完全不同的方向上解决问题。尽管有些人会说,但它们完全不同。

两者都解决了管理资源困难的问题。垃圾收集通过制作它来解决它,以便开发人员不需要花太多注意力来管理这些资源。 RAII通过使开发人员更容易关注他们的资源管理来解决它。任何说他们做同样事情的人都有卖给你的东西。

如果你看一下最近的语言趋势,你会发现这两种方法都使用同一种语言,因为坦白说,你真的需要这两个方面。你会看到许多使用各种垃圾收集的语言,这样你就不必注意大多数对象,而且这些语言也提供RAII解决方案(例如python&#39; s {{1}你真的想要注意它们的时间。

  • C ++通过构造函数/析构函数提供RAII,通过with提供GC(如果我可以提出引用计数和GC属于同一类解决方案,因为它们都旨在帮助您不需要注意寿命)
  • Python通过shared_ptr和GC通过引用计数系统和垃圾收集器提供RAII
  • C#通过withIDisposable提供RAII,通过分代垃圾收集器提供GC

各种语言都出现了模式。

答案 4 :(得分:10)

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

使用RAII,您知道在准确的时间资源将超出范围,您将清除一些内存,这将需要一些时间。但是,如果您不是垃圾收集器设置的主人,则无法预测清理何时会发生。

例如:使用GC可以更有效地清理一堆小对象,因为它可以释放大块,但它不会快速运行,并且很难预测何时会发生并且因为“大块清理” “它需要一些处理器时间,可能会影响您的程序性能。

答案 5 :(得分:9)

粗略地说。 RAII习惯用法可能更适合延迟抖动。垃圾收集器可能更适合系统的吞吐量

答案 6 :(得分:5)

&#34;高效&#34;在一个非常广泛的术语中,从开发的角度来看,RAII的效率通常低于GC,但就性能而言,GC通常比RAII效率低。但是,可以为这两种情况提供控制例子。当您在托管语言中拥有非常清晰的资源(de)分配模式时处理通用GC可能相当麻烦,就像使用RAII的代码在shared_ptr无缘无故地用于所有内容时效率低得惊人。

答案 7 :(得分:5)

关于一个或另一个是“有益的”还是更“有效”的问题的主要部分,如果不提供大量背景和争论这些术语的定义,就无法回答。

除此之外,你基本上可以感受到古代“Java或C ++是更好的语言”的紧张吗?在评论中发出嘶嘶声。我想知道这个问题的“可接受”答案是什么样的,并且最终很想看到它。

但是有一点关于可能重要的概念差异尚未被指出:使用RAII,你被绑定到调用析构函数的线程。如果您的应用程序是单线程的(即使是Herb Sutter表示The Free Lunch Is Over:今天大多数软件仍然 单线程),那么单个核心可能忙于处理清除不再与实际程序相关的对象...

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

(注意:有些答案已经尝试指出具有不同特征的应用程序模式,提到效率,性能,延迟和吞吐量 - 但尚未提及此特定点)

答案 8 :(得分:5)

垃圾收集和RAII都支持一个常见的构造,而另一个不适合。

在垃圾收集系统中,代码可以有效地将对不可变对象(例如字符串)的引用视为其中包含的数据的代理;传递这些引用几乎与传递“哑”指针一样便宜,并且比为每个所有者制作单独的数据副本或尝试跟踪数据的共享副本的所有权更快。此外,垃圾收集系统通过编写创建可变对象的类,根据需要填充它并提供访问器方法,可以轻松创建不可变对象类型,同时避免在构造函数中泄漏引用可能会使其变异的任何内容饰面。如果需要广泛复制对不可变对象的引用但对象本身不需要,则GC会击败RAII。

另一方面,RAII非常适合处理对象需要从外部实体获取专有服务的情况。虽然许多GC系统允许对象定义“Finalize”方法并在发现它们被放弃时请求通知,并且这些方法有时可能设法释放不再需要的外部服务,但它们很少可靠,无法提供令人满意的方式。确保及时发布外部服务。为了管理不可替代的外部资源,RAII击败GC。

GC获胜的案例与RAII获胜的案例之间的主要区别在于GC擅长管理可根据需要释放的可替代内存,但处理不可替代的资源却很差。 RAII擅长处理具有明确所有权的对象,但不善于处理除了包含数据之外没有真正身份的无主不可变数据持有者。

由于GC和RAII都不能很好地处理所有情况,因此语言可以为两者提供良好的支持。不幸的是,专注于一方的语言倾向于将另一方视为事后的想法。

答案 9 :(得分:4)

RAII统一处理可描述为资源的任何事物。动态分配就是这样一种资源,但它们绝不是唯一的资源,可以说不是最重要的资源。文件,套接字,数据库连接,gui反馈等等都可以通过RAII确定性地进行管理。

GCs只处理动态分配,减轻了程序员在程序生命周期内担心分配对象总量的问题(他们只需关心峰值并发分配量拟合)

答案 10 :(得分:1)

RAII和垃圾收集旨在解决不同的问题。

当您使用RAII时,您在堆栈上留下一个对象,唯一的目的是在离开方法范围时清理您想要管理的任何内容(套接字,内存,文件等)。这是针对 exception-safety ,而不仅仅是垃圾收集,这就是为什么你得到关于关闭套接字和释放互斥锁等的响应。 (好吧,所以除了我之外没有人提到互斥锁。)如果抛出异常,stack-unwinding会自然清理方法使用的资源。

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

垃圾收集还处理堆分配的对象,例如工厂构造对象的实例并返回它。在控制必须离开范围的情况下拥有持久对象是使垃圾收集具有吸引力的原因。但是您可以在工厂中使用RAII,因此如果在返回之前抛出异常,则不会泄漏资源。

答案 11 :(得分:0)

  

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

这是完全可行的 - 事实上,实际上已经完成 - 使用RAII(或使用普通的malloc / free)。你看,你不一定总是使用默认的分配器,它只能零碎地分配。在某些上下文中,您使用具有不同类型功能的自定义分配器。一些分配器具有内置的能力,可以同时释放某些分配器区域中的所有内容,而无需迭代单个分配的元素。

当然,你接下来的问题是什么时候解除所有的东西 - 是否使用那些分配器(或者它们与之关联的内存块是否必须是RAII,以及如何。) p>