为什么引用计数智能指针如此受欢迎?


52

如我所见,智能指针已在许多实际的C ++项目中广泛使用。

尽管某种智能指针显然有利于支持RAII和所有权转移,但也存在一种趋势,默认情况下使用共享指针作为“垃圾收集”方式,因此程序员不必考虑太多分配。

为什么共享指针比集成Boehm GC这样的适当垃圾收集器更受欢迎?(或者您是否完全同意它们比实际的GC更受欢迎?)

我知道常规GC相对于引用计数有两个优点:

  • 常规的GC算法在参考循环方面没有问题
  • 参考计数通常比适当的GC

使用引用计数智能指针的原因是什么?


6
我只是添加一条评论,指出这是使用错误的默认值:在大多数情况下,这std::unique_ptr是足够的,因此就运行时性能而言,其原始指针的开销为零。通过std::shared_ptr在所有地方使用,您还将使所有权语义模糊不清,从而失去了智能指针(除了自动资源管理之外)的主要好处之一-清楚地了解了代码的意图。
马特(Matt)

2
抱歉,这里接受的答案是完全错误的。引用计数具有较高的开销(计数而不是标记位,并且运行时性能较慢),雪崩递减时无限制的暂停时间,并且不像Cheney半空间那样复杂。
乔恩·哈罗普

Answers:


57

与垃圾收集相比,引用计数的一些优点:

  1. 低开销。垃圾收集器可能会很麻烦(例如,使程序在垃圾收集周期进行过程中在不可预知的时间冻结),并且会占用大量内存(例如,在垃圾收集最终开始之前,您的进程的内存占用不必要地增加了很多兆字节)

  2. 更可预测的行为。通过引用计数,可以确保在上一次引用消失后立即释放对象。另一方面,通过垃圾回收,当系统处理完该对象后,您的对象将“有时”被释放。对于RAM,在台式机或负载较小的服务器上,这通常不是大问题,但对于其他资源(例如,文件句柄),您通常需要尽快将其关闭,以避免以后发生潜在冲突。

  3. 更简单 引用计数可以在几分钟内得到解释,在一两个小时内即可实现。垃圾收集器,尤其是性能良好的垃圾收集器,非常复杂,没有多少人了解它们。

  4. 标准。C ++在STL中包括引用计数(通过shared_ptr)和朋友,这意味着大多数C ++程序员都熟悉它,并且大多数C ++代码都可以使用它。但是,没有任何标准的C ++垃圾收集器,这意味着您必须选择一个垃圾收集器,并希望它适合您的用例-否则,解决问题是您的问题,而不是语言的问题。

至于引用计数的缺点-不检测周期是一个问题,但是在过去十年中我从未亲自遇到过使用引用计数的问题。大多数数据结构自然是非循环的,如果确实遇到需要循环引用的情况(例如,树节点中的父指针),则可以仅使用weak_ptr或原始C指针作为“向后方向”。只要您在设计数据结构时就意识到潜在的问题,这不是问题。

至于性能,我从未对引用计数的性能有任何疑问。我在垃圾收集的性能方面遇到了问题,特别是GC可能导致的随机冻结,唯一的解决方案(“不分配对象”)也可能改为“不使用GC” 。


16
朴素的引用计数的实现通常得到很多比延迟为代价生产的GC(30-40%),较低的吞吐量。可以通过优化来缩小间隙,例如使用较少的refcount位,并避免跟踪对象直到它们逃逸-如果您主要是make_shared在返回时,C ++会自然地持续。延迟仍然是实时应用程序中更大的问题,但是吞吐量通常更为重要,这就是为什么跟踪GC如此广泛的原因。我不会很快就对他们说不好。
乔恩·普迪

3
我狡辩“简单”:在方面简单的执行所需的总金额是的,但不是简单的针对该代码使用它:比较告诉别人如何使用RC(“做这个创建对象时和销毁的时候他们” ),以了解如何(天真,这通常就足够了)使用GC('...')。
AakashM

4
“通过引用计数,可以确保在上一次引用消失后立即释放对象”。这是一个普遍的误解。flyingfrogblog.blogspot.co.uk/2013/10/…–
乔恩·哈罗普

4
@JonHarrop:那篇博客文章的观点很错误。您还应该阅读所有评论,尤其是最后一条。
Deduplicator

3
@JonHarrop:是的,有。他不明白,生命周期是整个范围的最后一部分。如果不再次使用该变量,根据注释有时只能起作用的F#优化有时会提前终止生命周期。这自然有其自身的危险。
Deduplicator

26

为了从GC中获得良好的性能,GC必须能够移动内存中的对象。在像C ++这样的语言中,您可以直接与内存位置进行交互,这几乎是不可能的。(Microsoft C ++ / CLR不算在内,因为它为GC管理的指针引入了新语法,因此实际上是另一种语言。)

Boehm GC虽然很漂亮,但实际上是两全其美的情况:您需要一个比好的GC慢的malloc(),因此您将失去确定性的分配/重新分配行为,而不会相应地提高世代GC的性能。另外,它必然是保守的,因此它不一定会收集所有垃圾。

良好且调试良好的GC可能很棒。但是在像C ++这样的语言中,收益微乎其微,而成本却往往不值得。

但是,有趣的是,随着C ++ 11变得越来越流行,lambda和捕获语义是否开始导致C ++社区面临导致Lisp社区在第一时间发明GC的相同类型的分配和对象生存期问题。地点。

另请参见我对StackOverflow上一个相关问题的回答


6
关于Boehm GC,我偶尔想知道它个人对C和C ++程序员传统上对GC的厌恶程度有多大的负担,仅仅是通过对总体技术的第一印象就不好了。
Leushenko

@Leushenko好说。一个很好的例子就是这个问题,Boehm gc被称为“适当的” gc,而忽略了它运行缓慢且实际上保证泄漏的事实。我在研究是否有人为shared_ptr实现了python样式的循环断路器时发现了这个问题,这听起来像是c ++实现的一个有价值的目标。
user4815162342 2015年

4

如我所见,智能指针已在许多实际的C ++项目中广泛使用。

没错,但客观地讲,现在绝大多数代码是用现代语言编写的,具有跟踪垃圾收集器的功能。

尽管某种智能指针显然有利于支持RAII和所有权转移,但也存在一种趋势,默认情况下使用共享指针作为“垃圾收集”方式,因此程序员不必考虑太多分配。

这是一个坏主意,因为您仍然需要担心周期。

为什么共享指针比集成Boehm GC这样的适当垃圾收集器更受欢迎?(或者您是否完全同意它们比实际的GC更受欢迎?)

哇,您的思路有很多错误:

  1. 从任何意义上讲,Boehm的GC都不是“适当的” GC。真可怕。它是保守的,因此会泄漏并且在设计上效率低下。请参阅:http//flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. 客观上讲,共享指针远没有GC普及,因为绝大多数开发人员现在都在使用GC语言,并且不需要共享指针。只需看看与C ++相比在就业市场中的Java和Javascript。

  3. 您似乎在限制使用C ++,因为我认为您认为GC是一个切线问题。并不是(获得良好的GC 的唯一方法是从一开始就为它设计语言和VM),所以您引入了选择偏见。真正想要正确进行垃圾收集的人不会坚持使用C ++。

使用引用计数智能指针的原因是什么?

您只能使用C ++,但希望您具有自动内存管理功能。


7
嗯,这是一个标记为c ++的问题,它讨论了C ++功能。显然,任何通用语句都 C ++代码中讨论,而不是整个编程。因此,“客观地”垃圾回收可能在C ++领域之外使用,这最终与手头的问题无关。
Nicol Bolas's

2
最后一行显然是错误的:您使用的是C ++,很高兴您没有被迫处理GC,而且延迟了资源的释放。苹果不喜欢GC是有原因的,关于GC语言的最重要的指导原则是:不要创建任何垃圾,除非您有闲置的资源或无法帮助它。
重复数据删除器

3
@JonHarrop:因此,比较具有和不具有GC的等效小程序,这些程序没有明确选择来发挥任何一方的优势。您期望哪一个需要更多的内存?
重复数据删除器2016年

1
@Deduplicator:我可以设想给出任何结果的程序。当程序被设计为在堆中保留内存(例如列表队列)之前保留堆分配内存时,引用计数的性能将优于跟踪GC,因为这对于世代GC而言是病理性能,并且会生成最多的浮动垃圾。当有许多小对象且生命周期较短但不是静态已知时,与基于范围的引用计数相比,跟踪垃圾收集将需要较少的内存,因此类似使用纯功能数据结构的逻辑程序之类的东西。
乔恩·哈罗普

3
@JonHarrop:如果您讲C ++,我的意思是使用GC(跟踪或其他方法)和RAII。其中包括引用计数,但仅在有用的地方。或者您可以与Swift程序进行比较。
重复数据删除器2016年

3

在MacOS X和iOS中,对于使用Objective-C或Swift的开发人员来说,引用计数很受欢迎,因为它是自动处理的,并且由于Apple不再支持垃圾回收,因此垃圾回收的使用已大大减少(有人告诉我,使用垃圾回收将在下一个MacOS X版本中中断,并且垃圾回收从未在iOS中实现)。实际上,我非常怀疑是否有很多软件在垃圾回收可用时使用垃圾回收。

摆脱垃圾回收的原因:在C风格的环境中,指针可能会“逃逸”到垃圾回收器无法访问的区域,因此它永远无法可靠地工作。苹果公司坚信并相信引用计数更快。(您可以在此处声明相对速度,但没有人能够说服Apple)。最后,没有人使用垃圾回收。

任何MacOS X或iOS开发人员要学习的第一件事是如何处理参考周期,因此对于真正的开发人员而言这不是问题。


以我的理解,并不是像C那样的环境决定了事情,而是GC是不确定的,需要更多的内存才能具有可接受的性能,而服务器/台式机外部总是有点稀缺。
Deduplicator

调试为什么垃圾收集器销毁了我仍在使用的对象(导致崩溃),这对我来说就决定 :-)
gnasher729

哦,是的,也可以。您到底找到原因了吗?
Deduplicator

是的,它是许多Unix函数之一,您在其中传递void *作为“上下文”,然后在回调函数中将其返回给您。void *实际上是一个Objective-C对象,垃圾收集器没有意识到该对象已藏在Unix调用中。调用回调,将void *转换为Object *,kaboom!
gnasher729

2

C ++中垃圾回收的最大缺点是,不可能正确地做到这一点:

  • 在C ++中,指针并不存在于自己的围墙社区中,它们与其他数据混合在一起。因此,您无法将指针与恰好具有可以解释为有效指针的位模式的其他数据区分开。

    后果:任何C ++垃圾收集器都会泄漏应收集的对象。

  • 在C ++中,您可以执行指针算术以派生指针。这样,如果找不到指向块开头的指针,则并不意味着不能引用该块。

    后果:任何C ++垃圾回收器都必须考虑这些调整,将碰巧指向块内任何位置的任何位序列(包括紧随其结尾之后)都视为引用该块的有效指针。

    注意:没有C ++垃圾收集器可以使用以下技巧来处理代码:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;

    是的,这会引发未定义的行为。但是,某些现有代码比它的优点更聪明,并且可能会触发垃圾回收器进行初步的重新分配。


2
它们与其他数据混合。 ”与其说它们与其他数据“混合”还不算多。使用C ++类型系统很容易看到什么是指针,什么不是指针。问题是指针经常成为其他数据。对于许多C风格的API,将指针隐藏在整数中是一种不幸的常用工具。
Nicol Bolas's

1
您甚至不需要不确定的行为即可在c ++中搞乱垃圾收集器。例如,您可以序列化指向文件的指针并在以后读取它。同时,您的进程可能不在其地址空间中的任何地方包含该指针,因此垃圾回收器可以收集该对象,然后在您反序列化该指针时...糟糕。
Bwmat

@Bwmat“偶”?将指针写到这样的文件似乎有点……牵强。无论如何,同样严重的问题困扰着指向堆栈对象的指针,当您从代码中其他位置的文件中读取指针时,它们可能消失了!反序列化无效的指针值未定义的行为,请不要这样做。
海德

如果这样做的话,如果您正在做类似的事情,您将需要小心。这只是一个示例,通常来说,垃圾收集器不能在所有情况下都在C ++中“正常”工作(不更改语言)
Bwmat

1
@ gnasher729:嗯,不是吗?过去的指针完全可以吗?
Deduplicator
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.