Raii vs.垃圾收集器

RAII vs. Garbage Collector

本文关键字:收集器 vs Raii      更新时间:2023-10-16

我最近观看了Herb Sutter关于"泄漏免费C ..."的精彩演讲以及他们如何解决大多数内存泄漏问题。

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

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

虽然两者都处理分配,但他们以完全不同的方式进行了分配。如果您像Java中的gc一样,添加了自己的开销,从资源发布过程中删除了一些确定论,并处理循环引用。

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

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

此确定性是GC根本不允许的功能。有时您希望能够知道,在某个点进行了清理操作之后(删除临时文件,关闭网络连接等(。

在这种情况下,GC没有切割它,这是C#中的原因(例如(您具有IDisposable接口。

我什至听说拥有垃圾收集器可以更有效,因为它可以一次释放更大的内存,而不是在整个代码上释放小记忆。

可以...取决于实现。

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

这给出了两个优点。首先,RAII无法解决某些类型的问题。以我的经验,这些是罕见的。

更大的是,它使程序员可以懒惰,并且不在乎 对内存资源寿命和某些其他您不介意延迟清理的资源。当您不必关心某些类型的问题时,您可以更多地关心其他问题。这使您可以专注于要关注的问题的部分。

不利的一面是,如果没有RAII,管理您想要限制的寿命的资源很难。GC语言基本上将您降低到具有极为简单的范围结合的寿命,或者要求您手动进行资源管理,例如在C中,手动说明您已经完成了资源。它们的对象寿命系统与GC密切相关,并且对于大型复合物(但不含周期(系统的严格终身管理都不佳。

公平,C 中的资源管理需要在如此大的复杂(但无周期(的系统中正确地完成工作。c#和类似语言只是使它变得更加困难,作为交换,它们使情况变得容易。

大多数GC实施也迫使非局部性全身类别;创建一般对象的连续缓冲区,或将一般对象组合到一个较大的对象中,这并不是大多数GC实现使其变得容易的东西。另一方面,C#允许您创建具有有限功能的值类型struct s。在当前的CPU架构时代,Cache友善是关键,而当地GC部队的缺乏是沉重的负担。由于这些语言在大多数情况下具有字节码运行时,因此,从理论上讲,JIT环境可以将常用的数据移动在一起,但是与C 相比,由于常见的高速缓存失误,您通常只是获得统一的性能损失。

GC的最后一个问题是DealLocation是不确定的,有时会导致性能问题。现代GC使这比过去更少。

raii和GC在完全不同的方向上解决问题。尽管有些人会说什么。

都完全不同。

都解决了管理资源很难的问题。垃圾收集通过制造来解决它,以便开发人员不需要对管理这些资源的关注。RAII通过使开发人员更容易关注其资源管理来解决它。任何说自己做同样事情的人都有可以卖给你的东西。

如果您查看语言的最新趋势,则看到两种方法都使用相同的语言使用,因为坦率地说,您确实需要拼图的两面。您正在看到很多使用垃圾收集的语言,因此您不必关注大多数对象,而这些语言还为您真正想注意的时代提供了RAII Solutions(例如Python的with操作员(给他们。

  • C 通过构造函数/驱动器和GC通过shared_ptr提供RAII(如果我可以提出重新计算和GC在同一类解决方案中的论点,因为它们都旨在帮助您不需要注意寿命(<(
  • Python通过with和GC通过倒数系统和垃圾收集器提供RAII
  • c#通过IDisposableusing和GC提供RAII,并通过世代垃圾收集器

模式在每种语言中都出现。

请注意,RAII是一个编程成语,而GC是一种内存管理技术。因此,我们将苹果与橙子进行比较。

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

所谓的基于RAII的内存管理技术(至少在考虑内存资源并忽略其他文件(例如文件(和真正的垃圾收集技术时,这确实是指参考计数(和的处理圆形参考(用于循环图(。

通过参考计数,您需要专门为它们编码(使用弱参考或其他内容(。

在许多有用的情况下(考虑std::vector<std::map<std::string,int>>(,参考计数是隐式的(因为它只能是0或1(,并且实际上省略计数位(实际上缺乏(。在std::shared_ptr中,有一个真正的参考计数器。但是,内存仍然是隐式手动管理的( newdelete触发了构造函数和破坏者内部(,但是"隐式" delete(在破坏者中(给出了自动内存管理的幻觉。但是,对newdelete的电话仍在发生(它们花费时间(。

顺便说一句,GC 实施可以(并且经常(以某种特殊的方式处理循环,但是您将这种负担留给了GC(例如,阅读有关Cheney's算法的信息(。

(。

某些GC算法(尤其是世代复制垃圾收集器(不要费心释放个体对象的内存,它将在复制后发布 en asse 。实际上

一些GC提供了最终化(主要用于管理 nonmemory 外部资源(如文件((,但是您很少使用它(因为大多数值仅消耗内存资源(。缺点是最终确定没有提供任何时间保证。实际上,使用最终确定的程序将其用作最后一项度假胜地(例如,文件的关闭应该或多或少地在最终确定之外进行,也应该与之发生。

(。

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

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

在C 代码中,您可以使用Boehm的GC或Ravenbrook的MPS或代码自己的追踪垃圾收集器。当然,使用GC是一种权衡(例如,不确定性,缺乏时机保证等,都有一些不便...(。

(。

我认为RAII在所有情况下都不是处理记忆的最终方法。在某些情况下,将您的程序编码在真正有效的GC实现(想想OCAML或SBCL(中可以比C 17中的精美RAII样式更简单(开发(,更快地(执行((执行(。在其他情况下不是。YMMV。

作为一个例子,如果您用最奇特的RAII样式在C 中编码方案解释器,则您仍然需要编码(或使用(aexpect a expect explicit gc堆有圆形(。大多数证明助手是用GC-ED语言编码的,通常是功能性的(我知道的唯一用C 编码的一种(是有充分理由的。

顺便说一句,我有兴趣找到这样的C 17方案的实现(但对自己进行编码不太感兴趣(,最好具有某些多线程功能。

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

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

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

大概是说话。raii习惯可能更好,对于延迟 jitter 。垃圾收集器可能对系统的吞吐量

更好

"有效"是一个非常广泛的术语,在开发意义上,raii通常比GC效率低,但就性能而言,GC的效率通常低于RAII。但是,可以为这两种情况提供概况。处理通用GC时,当您拥有非常清晰的资源(DE(分配模式时,托管语言的分配模式可能会很麻烦,就像使用RAII的代码无缘无故地使用shared_ptr时,使用RAII的效率很小。

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

在垃圾收集的系统中,代码可以有效地将对不可变的对象(例如字符串(的引用视为其中包含的数据的代理;传递此类参考文献几乎与传递"愚蠢"指针一样便宜,并且比为每个所有者制作数据的单独副本或试图跟踪数据共享副本的所有权要快。此外,垃圾收集的系统使创建不变的对象类型可以轻松创建一个可变的对象,根据需要将其填充并提供访问器方法,同时避免将一旦构造函数变为构造函数的任何内容,完成。如果需要广泛复制对不变物体但对象本身的引用,则GC击败了Raii。

另一方面,RAII非常出色地处理对象需要从外部实体获得独家服务的情况。尽管许多GC系统允许对象定义"最终化"方法并在发现被放弃时请求通知,并且这些方法有时可能设法释放不再需要的外部服务,但它们很少可靠,以提供令人满意的方式确保及时发布外部服务。为了管理不可杀死的外部资源,RAII击败了GC的手。

GC获胜与RAII获胜的情况之间的主要区别在于,GC擅长管理可及时的记忆,这些记忆可以在需要的基础上释放,但在处理无填补资源方面很差。RAII擅长处理具有清晰所有权的物体,但不擅长处理所有者不变的数据持有人,除了它们所包含的数据外,没有真正的身份。

由于GC和RAII都不能很好地处理所有场景,因此对语言为他们俩提供良好支持是有帮助的。不幸的是,专注于一种的语言倾向于将另一种人视为事后的想法。

问题的主要部分是关于一个人是否有益的问题如果没有给出很多上下文并争论这些术语的定义,则不能回答"或更高的"有效"。

除此之外,您基本上可以感觉到古代的" Java或C 是更好的语言?"弗拉梅瓦(Flamewar(在评论中crack啪作响。我想知道这个问题的"可接受"答案可能会是什么样,很好奇最终。

但是,关于可能重要的概念性差异的一个点尚未指出:使用RAII,您与称为Destructor的线程相关。如果您的应用程序是单线螺纹(即使是Herb Sutter表示免费午餐已经结束:当今大多数软件实际上仍然是是单线线程(,那么一个核心可能忙于处理与实际程序不再相关的对象的清理...

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

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

raii均匀地处理任何可描述为资源的事物。动态分配就是这样一种资源,但它们绝不是唯一的资源,可以说不是最重要的资源。文件,插座,数据库连接,GUI反馈等都是可以通过RAII确定管理的所有内容。

GCS仅处理动态分配,减轻程序员担心程序生命周期内分配的对象的总体积(他们只需要关心峰值并发分配量拟合(

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

当您使用raii时,您将对象留在堆栈上,其目的是清理要管理的任何内容(插座,内存,文件等(,以保留该方法的范围。这是异常安全,而不仅仅是垃圾收集,这就是为什么您会得到有关关闭插座和释放静音等的响应的原因。(好吧,所以除了我之外,没有人提到静音。(如果抛出了例外,堆叠不伴随自然会清理方法。

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

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

我什至听说拥有垃圾收集器可以更有效,因为它可以一次释放更大的内存,而不是在整个代码上释放小记忆。

这是完全可行的 - 实际上,实际上是使用RAII(或普通的Malloc/Free(完成的。您会看到,您不一定总是使用默认分配器,而默认分配器只能零碎。在某些情况下,您使用具有不同功能的自定义分配器。某些分配者具有一次内部释放某些分配区域的内在能力,而无需迭代单个分配的元素。

当然,您会遇到何时处理所有内容的问题 - 是否使用这些分配器(或与之关联的记忆板必须予以rai rai.