Jid*_*doo 74 c++ garbage-collection memory-leaks smart-pointers
我最近在CppCon 2016上观看了Herb Sutter关于"Leak Free C++ ..."的精彩演讲,他谈到了使用智能指针实现RAII(资源获取是初始化) - 概念以及它们如何解决大多数内存泄漏问题.
现在我在想.如果我严格遵循RAII规则,这似乎是一件好事,为什么这与C++中的垃圾收集器有什么不同呢?我知道使用RAII,程序员可以完全控制何时再次释放资源,但是在任何情况下都只对垃圾收集器有益吗?它的效率真的会降低吗?我甚至听说有一个垃圾收集器可以更高效,因为它可以一次释放更大的内存块,而不是在代码中释放小内存块.
utn*_*tim 61
如果我严格遵循RAII规则,这似乎是一件好事,为什么这与C++中的垃圾收集器有什么不同呢?
虽然两者都涉及分配,但它们以完全不同的方式进行.如果您正在考虑使用Java中的GC,那会增加自己的开销,从资源释放过程中删除一些确定性并处理循环引用.
对于特定情况,您可以实现GC,具有不同的性能特征.我在高性能/高吞吐量服务器中实现了一次关闭套接字连接(仅调用套接字关闭API花了太长时间并且提高了吞吐量性能).这不涉及内存,而是网络连接,也没有循环依赖性处理.
我知道使用RAII,程序员可以完全控制何时再次释放资源,但是在任何情况下都只对垃圾收集器有益吗?
这种确定性是GC根本不允许的功能.有时您希望能够知道在某一点之后,已执行清理操作(删除临时文件,关闭网络连接等).
在这种情况下,GC不会削减它,这就是C#(例如)你有IDisposable接口的原因.
我甚至听说有一个垃圾收集器可以更高效,因为它可以一次释放更大的内存块,而不是在代码中释放小内存块.
可以......取决于实施.
Yak*_*ont 38
垃圾收集解决了RAII无法解决的某些类别的资源问题.基本上,它归结为循环依赖关系,您不会事先识别循环.
这给它带来了两个好处.首先,RAII无法解决某些类型的问题.根据我的经验,这些是罕见的.
更大的一点是它让程序员变得懒惰而不关心内存资源的生命周期以及你不介意延迟清理的某些其他资源.当您不必关心某些类型的问题时,您可以更关心其他问题.这使您可以专注于您想要关注的问题部分.
不利的一面是,如果没有RAII,那么管理您希望受限制的资源很难.GC语言基本上可以将您简化为具有极其简单的范围限制生命周期,或者要求您手动执行资源管理(如C语言),并手动声明您已完成资源.它们的对象生命周期系统与GC密切相关,并且不适用于大型复杂(无循环)系统的严格生命周期管理.
公平地说,C++中的资源管理需要大量工作才能在如此大的复杂(无循环)系统中正常完成.C#和类似的语言只是让它变得更加难以接受,作为交换,它们使简单易用.
大多数GC实现也会强制非本地化的完整类; 创建一般对象的连续缓冲区,或将一般对象组合成一个更大的对象,并不是大多数GC实现变得容易的事情.另一方面,C#允许您创建struct具有有限功能的值类型.在当前的CPU架构时代,缓存友好性是关键,而GC部队缺乏局部性是一个沉重的负担.由于这些语言大部分都具有字节码运行时,理论上JIT环境可以将常用数据一起移动,但是与C++相比,由于频繁的缓存未命中,您通常会获得统一的性能损失.
GC的最后一个问题是解除分配是不确定的,有时会导致性能问题.与过去相比,现代地理信息系统使问题变得更少.
Bas*_*tch 14
请注意,RAII是一种编程习惯,而GC是一种内存管理技术.所以我们将苹果与橙子进行比较.
但是,我们可以限制RAII到它的内存管理方面唯一的和比较,为GC技术.
所谓的基于RAII的内存管理技术(实际上意味着引用计数,至少在您考虑内存资源并忽略其他文件如文件时)和真正的垃圾收集技术之间的主要区别在于循环引用的处理(对于循环图) .
使用引用计数,您需要专门为它们编码(使用弱引用或其他东西).
在许多有用的情况下(想到std::vector<std::map<std::string,int>>),引用计数是隐式的(因为它只能是0或1)并且实际上是省略的,但是构造函数和析构函数(对RAII必不可少)的行为就好像有一个引用计数位(几乎没有).在std::shared_ptr那里有一个真正的参考柜台.但内存仍然是隐式 手动管理(在构造函数和析构函数中使用new并delete触发),但"隐式" delete(在析构函数中)给出了自动内存管理的错觉.然而,调用new和delete还是发生了(而且花费的时间).
BTW GC 实现可能(并且经常)以某种特殊方式处理循环,但是你将这个负担留给GC(例如,阅读切尼的算法).
一些GC算法(尤其是代复制垃圾收集器)也懒得释放内存单独的对象,它是释放集体副本之后.在实践中,Ocaml GC(或SBCL)可以比真正的C++ RAII编程风格更快(对于某些,而不是所有类型的算法).
有些GC提供了最终化(主要用于管理非内存外部资源,如文件),但您很少使用它(因为大多数值仅消耗内存资源).缺点是最终确定不提供任何时间保证.实际上,使用finalization的程序正在使用它作为最后的手段(例如,文件的关闭仍应在最终确定之外或多或少明确地发生,并且还与它们一起).
您仍然可以使用GC(以及RAII,至少在使用不当时)内存泄漏,例如,当某个值保留在某个变量或某个字段中但将来永远不会使用时.它们发生的频率较低.
我建议阅读垃圾收集手册.
在您的C++代码中,您可以使用Boehm的GC或Ravenbrook的MPS或编写您自己的跟踪垃圾收集器.当然使用GC是一种权衡(存在一些不便,例如非确定性,缺乏时序保证等等).
我不认为RAII是在所有情况下处理记忆的最终方式.在一些情况下,在真正有效的GC实现中编写程序(想想Ocaml或SBCL)可以比在C++ 17中使用花哨的RAII样式编码更简单(开发)和更快(执行).在其他情况下,它不是.因人而异.
例如,如果您使用最高级的RAII样式在C++ 17中编写Scheme解释器,您仍然需要在其中编码(或使用)显式 GC(因为Scheme堆具有圆形).大多数证明助手都是用GC编辑的语言编写的,通常是功能性的语言(我知道唯一一个用C++编写的语言是精益的),原因很充分.
顺便说一句,我有兴趣找到这样一个Scheme的C++ 17实现(但对自己编码不太感兴趣),最好有一些多线程能力.
Cor*_*ica 13
RAII和GC在完全不同的方向上解决问题.尽管有些人会说,但它们完全不同.
两者都解决了管理资源困难的问题.垃圾收集通过制作它来解决它,以便开发人员不需要像管理这些资源那样多关注.RAII通过使开发人员更容易关注他们的资源管理来解决它.任何说他们做同样事情的人都有卖给你的东西.
如果你看看最近的语言趋势,你会发现这两种方法都使用相同的语言,因为坦白说,你真的需要这两个方面.你会看到许多使用各种垃圾收集的语言,这样你就不必关注大多数对象,而且这些语言也提供RAII解决方案(比如python的with运算符),这是你真正想要关注的时候.他们.
shared_ptr(如果我可以认为refcounting和GC属于同一类解决方案,因为它们都旨在帮助您不必关注生命周期)with通过引用计数系统和垃圾收集器提供RAII 和GCIDisposable并using通过代垃圾收集器和GC每种语言都出现了模式.
tty*_*ty6 10
关于垃圾收集器的一个问题是很难预测程序性能.
使用RAII,您知道在准确的时间资源将超出范围,您将清除一些内存,这将需要一些时间.但是,如果您不是垃圾收集器设置的主人,则无法预测清理何时会发生.
例如:使用GC可以更有效地清理一堆小对象,因为它可以释放大块,但它不会快速运行,并且很难预测何时会发生并且由于"大块清理"它将占用一些处理器时间会影响程序性能.
"高效"是一个非常广泛的术语,在开发意义上,RAII通常效率低于GC,但就性能而言,GC通常比RAII效率低.但是,可以为这两种情况提供控制例子.当您在托管语言中拥有非常清晰的资源(de)分配模式时处理通用GC可能相当麻烦,就像使用RAII的代码在shared_ptr无缘无故地用于所有内容时可能会出乎意料地效率低下.
关于一个或另一个是"有益的"还是更"有效"的问题的主要部分在没有提供大量背景和争论这些术语的定义的情况下无法回答.
除此之外,你基本上可以感受到古代"Java或C++是更好的语言"的张力吗?在评论中发出嘶嘶声.我想知道这个问题的"可接受"答案是什么样的,并且很想最终看到它.
但是有一点关于可能重要的概念差异还没有被指出:使用RAII,你被绑定到调用析构函数的线程.如果您的应用程序是单线程的(尽管Herb Sutter表示免费午餐已经结束:今天大多数软件仍然是单线程的),那么单个核心可能正忙着处理没有的对象的清理与实际计划相关的时间更长......
与此相反,垃圾收集器通常在其自己的线程中运行,甚至在多个线程中运行,因此(在某种程度上)与其他部分的执行分离.
(注意:一些答案已经尝试指出具有不同特征的应用程序模式,提到效率,性能,延迟和吞吐量 - 但这个特定点尚未提及)
垃圾收集和RAII每个都支持一个共同的构造,另一个不适合.
在垃圾收集系统中,代码可以有效地将对不可变对象(例如字符串)的引用视为其中包含的数据的代理; 传递这些引用几乎与传递"哑"指针一样便宜,并且比为每个所有者制作单独的数据副本或尝试跟踪数据的共享副本的所有权更快.此外,垃圾收集系统通过编写创建可变对象的类,根据需要填充它并提供访问器方法,可以轻松创建不可变对象类型,同时避免在构造函数中泄漏引用可能会使其变异的任何内容饰面.如果需要广泛复制对不可变对象的引用但对象本身不需要,则GC会击败RAII.
另一方面,RAII非常适合处理对象需要从外部实体获取专有服务的情况.虽然许多GC系统允许对象定义"Finalize"方法并在发现它们被放弃时请求通知,并且这些方法有时可能设法释放不再需要的外部服务,但它们很少可靠,无法提供令人满意的方式.确保及时发布外部服务.为了管理不可替代的外部资源,RAII击败了GC.
GC获胜的案例与RAII获胜的案例之间的主要区别在于GC擅长管理可根据需要释放的可替代内存,但处理不可替代的资源却很差.RAII擅长处理具有明确所有权的对象,但不善于处理除了包含数据之外没有真正身份的无主不可变数据持有者.
由于GC和RAII都不能很好地处理所有场景,因此语言可以为这两种场景提供良好的支持.不幸的是,专注于一个语言的语言倾向于将另一个视为事后的想法.
| 归档时间: |
|
| 查看次数: |
8577 次 |
| 最近记录: |