如果存在智能指针,为什么要进行垃圾回收


67

如今,垃圾收集了许多语言。第三方甚至可以使用C ++。但是C ++具有RAII和智能指针。那么使用垃圾回收有什么意义呢?它在做额外的事情吗?

在其他语言(例如C#)中,如果所有引用都被视为智能指针(不考虑RAII),那么按照规范和实现,是否还会需要垃圾收集器?如果否,那为什么不是呢?


1
在问了这个问题后,我明白了一件事- 智能指针需要RAII来管理自动释放
Gulshan 2010年

8
智能指针意味着将RAII用于GC;)
Dario

嘿,C#应该可以选择使用RAII处理所有“垃圾收集”。可以在应用程序关闭时检测到循环引用,我们所需要的只是查看在释放Program.cs类之后哪些分配仍在内存中。然后,可以将循环引用替换为某种星期引用。
AareP'2

Answers:


67

那么,使用垃圾回收有什么意义呢?

我假设您的意思是引用计数的智能指针,并且我会注意到它们是(基本的)垃圾收集形式,因此我将回答“其他形式的垃圾收集相对于引用计数的智能指针有何优势”的问题代替。

  • 准确性。引用计数本身会泄漏周期,因此,除非添加其他技术来捕获周期,否则引用计数的智能指针通常会泄漏内存。一旦添加了这些技术,引用计数的简单性优势便消失了。另外,请注意,基于范围的引用计数和跟踪GC在不同时间收集值,有时引用计数收集较早,而有时跟踪GC收集较早。

  • 吞吐量。智能指针是垃圾收集效率最低的形式之一,尤其是在多线程应用程序中,引用计数被原子地增加时,尤其如此。有一些旨在减轻这种情况的高级参考计数技术,但跟踪GC仍然是生产环境中的首选算法。

  • 延迟时间。典型的智能指针实现使析构函数发生雪崩,从而导致无限的暂停时间。其他形式的垃圾收集更具增量性,甚至可以是实时的,例如贝克的跑步机。


23
简直不敢相信这个答案就是最重要的答案。它显示出完全缺乏对C ++智能指针的理解,并使声明与现实不同步,以至于它简直荒谬。首先,在精心设计的C ++代码段中最主要的智能指针是唯一指针,而不是共享指针。en.cppreference.com/w/cpp/memory/unique_ptr 其次,我无法相信您实际上声称非确定性垃圾回收比智能指针具有“性能”优势和实时优势。
user1703394 '16

4
@ user1703394看来应答者在脑海中共享了指针(对还是错,我不太确定OP的建议),其性能要比非确定性垃圾回收低。
内森·库珀

8
这些都是稻草人的论点,仅当您完全忽略实际问题或忽略不同类型的智能指针的实际使用模式时才有效。问题是关于智能指针。是的,shared_ptr是一个智能指针,是的,shared_ptr是最昂贵的智能指针,但是不,在任何使性能参数接近相关的地方都没有普遍使用它们的实际参数。认真地,这个答案应该转移到关于引用计数的问题。对于一个好的智能指针问题,我的参考引用计数不高。
user1703394 '16

4
“不是真正的指针不是更广泛的概念”,是吗?您不知道该语句在多大程度上破坏了您到目前为止提出的所有可能有效的论点。也许看看Rust的所有权并移动语义:koerbitz.me/posts/… 具有C ++历史经验的人很容易错过C ++ 11 / C ++ 14的内存模型,改进了智能功能这一事实。指针和移动语义与它们的前身完全不同。看看Rust是如何做到的,它比C ++干净,并提供了新的视角。
user1703394

6
@ user1703394:“由于析构函数导致的无限制的暂停是RAII用于非内存资源的不幸属性”。不,这与非内存资源无关。
乔恩·哈罗普

63

既然没有人从这个角度看过它,那么我将重新表述您的问题:如果可以在库中进行操作,为什么还要在语言中添加一些内容?忽略特定的实现和语法细节,GC /智能指针基本上是该问题的特例。如果可以在库中实现垃圾收集器,为什么还要用语言本身定义垃圾收集器?

这个问题有两个答案。最重要的第一:

  1. 您确保所有代码都可以使用它进行互操作。我认为,这是直到Java / C#/ Python / Ruby才真正实现代码重用和代码共享重要原因。库需要进行通信,并且它们唯一可靠的共享语言就是语言规范本身(在某种程度上还包括其标准库)中的内容。如果您曾经尝试过在C ++中重用库,则可能会遇到标准内存语义没有引起的可怕痛苦。我想将结构传递给某个lib。我可以通过参考吗?指针?scoped_ptrsmart_ptr?我是否通过所有权?有没有办法表明这一点?如果库需要分配怎么办?我必须给它分配器吗?通过不使内存管理成为语言的一部分,C ++迫使每对库都必须在这里协商自己的特定策略,而且很难让所有人都同意。GC保证完全不发行。

  2. 您可以围绕它设计语法。因为C ++本身并不封装内存管理,所以它必须提供一系列语法挂钩,以使用户级代码可以表达所有细节。您有指针,引用,,const解引用运算符,间接运算符,地址等。如果将内存管理引入语言本身,则可以围绕该语言设计语法。所有这些运算符都消失了,并且语言变得更简洁了。

  3. 您可以获得很高的投资回报率。给定代码段生成的值乘以使用它的人数。这意味着您拥有的用户越多,您花得更多的钱可以花在一件软件上。将功能移入语言后,该语言的所有用户都将使用它。这意味着您可以比仅分配给那些用户的一部分使用的库分配更多的精力。这就是为什么Java和C#之类的语言拥有绝对一流的VM和极好的高质量垃圾收集器的原因:开发它们的成本分摊到数百万用户中。


很棒的答案!如果我能投票不止一次...
Dean Harding

10
值得注意的是,垃圾回收实际上不是在C#语言本身中实现的,而是在.NET Framework中,尤其是在公共语言运行时(CLR)中实现的。
罗伯特·哈维

6
@RobertHarvey:它不是由该语言实现的,但是如果没有该语言的合作,它将无法正常工作。例如,编译器必须包括在代码的每个点上指定用于保存对未固定对象的引用的每个寄存器或堆栈帧偏移的位置的信息。那是绝对的无例外,无论什么不变,如果没有语言支持就无法维持。
2013年

使GC支持该语言和所需框架的主要优点是,它确保不会存在对可能为其他目的分配的内存引用。如果调用Dispose封装了位图的对象,则对该对象的任何引用将是对已处置位图对象的引用。如果在其他代码仍希望使用该对象的情况下过早删除该对象,则位图类可以确保其他代码将以可预测的方式失败。相比之下,使用对已释放内存的引用是未定义行为。
2013年

34

垃圾回收基本上只是意味着您分配的对象在无法访问之后的某个时候自动释放。

更准确地说,它们在程序无法访问时被释放,因为循环引用的对象永远不会被释放。

智能指针仅指行为类似于普通指针但具有一些附加功能的任何结构。这些包括但不限于解除分配,还包括写时复制,绑定检查,...

现在,如您所述,智能指针可用于实现垃圾收集的一种形式。

但是思路如下:

  1. 垃圾收集是一件很酷的事情,因为它很方便,我需要减少的事情
  2. 因此:我想用我的语言收集垃圾
  3. 现在,如何使GC成为我的语言?

当然,您可以从一开始就设计它。C#被设计为垃圾回收的,因此只有new您的对象,并且在引用超出范围时将被释放。如何完成此操作取决于编译器。

但是在C ++中,没有垃圾回收的意图。如果我们分配了一些指针int* p = new int;,但它不在范围之内,p则会将自身从堆栈中删除,但是没有人会照顾分配的内存。

现在,您从一开始就只有确定性析构函数。当对象离开其创建范围时,将调用其析构函数。结合模板和运算符重载,您可以设计一个包装器对象,其行为类似于指针,但是使用析构函数来清理附加到其上的资源(RAII)。您将此称为智能指针

这都是高度C ++特定的:运算符重载,模板,析构函数,...在这种特定的语言情况下,您已经开发了智能指针来为您提供所需的GC。

但是,如果您从一开始就使用GC设计语言,那么这仅仅是实现细节。您只是说对象将被清除,编译器将为您完成此操作。

像C ++这样的智能指针在像C#这样的语言中甚至不可能实现,而C#根本没有确定性的破坏(C#通过提供用于调用.Dispose()某些对象的语法糖来解决此问题)。未引用的资源最终将由GC回收,但是确切何时会发生未定义的资源。

反过来,这可以使GC更有效率地完成工作。.NET GC的语言比设置在其之上的智能指针更深入地构建在语言中,例如,可以延迟内存操作并分块执行它们,以使其更便宜,甚至可以移动内存以提高效率,具体取决于对象的使用频率被访问。


C#通过IDisposable和确实具有确定性破坏的形式using。但这需要程序员一点点的努力,这就是为什么它通常仅用于非常稀缺的资源(例如数据库连接句柄)的原因。
JSBձոգչ2010年

7
@JSBangs:是的。就像C ++围绕RAII构建智能指针以获取GC一样,C#也采用另一种方式并围绕GC构建“智能处理器”以获取RAII;)实际上,令人遗憾的是RAII在C#中如此困难,因为它对于异常处理非常有用-安全的资源处理。例如,F#尝试IDisposable通过将常规替换let ident = valueuse ident = value... 来尝试更简单的语法...
Dario

@Dario:“ C#走了另一条路,并在GC周围构建了“智能处理器”以获取RAII”。C#中的RAII using与垃圾回收完全无关,它只是在变量超出范围时才调用函数,就像C ++中的析构函数一样。
乔恩·哈罗普

1
@乔恩·哈罗普:请问什么?您引用的语句是关于纯C ++析构函数的,不涉及任何引用计数/智能指针/垃圾回收。
达里奥

1
“垃圾回收基本上只是意味着,当您不再分配引用的对象时,它们就会自动释放。更准确的说,当程序无法访问它们时,它们就会被释放,否则循环引用的对象将永远不会被释放。” ...更准确的是说,它们会自动发布在某一点后,没有的时候。请注意,表示开垦立即发生时,而实际上开垦通常在以后发生。
Todd Lehman

4

我认为,垃圾回收和用于内存管理的智能指针之间有两个大区别:

  1. 智能指针不能收集循环垃圾。垃圾收集罐
  2. 智能指针在应用程序线程上进行引用,解引用和重新分配时完成所有工作。垃圾收集不需要

前者意味着GC将收集智能指针不会收集的垃圾。如果您使用的是智能指针,则必须避免创建此类垃圾,或者准备手动进行处理。

后者意味着无论智能指针多么聪明,它们的操作都会减慢程序中的工作线程。垃圾回收可以延迟工作,并将其移至其他线程;这样就可以提高整体效率(实际上,即使没有智能指针的额外开销,现代GC的运行时成本也比普通的malloc / free系统要少),并且无需花费大量时间即可完成它仍需要做的工作应用程序线程的方式。

现在,请注意,作为程序构造的智能指针可用于执行各种其他有趣的操作-参见Dario的答案-完全超出了垃圾回收的范围。如果要执行这些操作,则需要智能指针。

但是,出于内存管理的目的,我看不到智能指针取代垃圾回收的任何前景。他们根本不那么擅长。


6
@Tom:看一下Dario的答案,了解有关智能指针的详细信息。至于智能指针的优点–确定性释放在用于控制资源(不仅是内存)时可能是一个巨大的优势。实际上,这已被证明非常重要,因此 Microsoft using在后续C#版本中引入了该块。此外,在实时系统中可能会禁止GC的不确定行为(这就是为什么不在那里使用GC的原因)。另外,不要忘了GC 复杂而无法正确处理,以至于实际上大多数内存泄漏并且效率很低(例如Boehm…)。
康拉德·鲁道夫

6
我认为,GC的不确定性有点让人讨厌-有些GC系统适合实时使用(例如IBM的Recycler),即使您在台式机和服务器VM中看不到这些系统也是如此。另外,使用智能指针意味着使用malloc / free,并且由于需要搜索空闲列表,因此malloc的常规实现是不确定的。与不带malloc / free的系统相比,移动式GC系统具有更多的确定性分配时间,尽管当然,较少的确定性解除分配时间。
汤姆·安德森

3
至于复杂性:是的,GC很复杂,但是我不知道“实际上实际上是泄漏内存并且效率很低”,因此有兴趣查看其他证据。Boehm并不是证据,因为它是一个非常原始的实现,并且是为服务C语言而构建的,由于缺乏类型安全性,从根本上讲不可能提供准确的GC。这是一个勇敢的努力,并且确实奏效,但您不能将其视为GC的典范。
汤姆·安德森

8
@乔恩:绝对不是胡扯。bugzilla.novell.com/show_bug.cgi?id=621899或更笼统地说:flyingfrogblog.blogspot.com/2009/01/…这是众所周知的,并且是所有保守GC的属性。
康拉德·鲁道夫

3
“现代GC的运行时成本低于普通的malloc / free系统。” 红鲱鱼在这里。这仅仅是因为传统的malloc是一种效率极低的算法。对于不同的块大小使用多个存储桶的现代分配器分配起来要快得多,发生堆碎片的可能性要小得多,并且仍然可以为您带来快速的重新分配。
梅森惠勒

3

术语垃圾收集意味着有任何垃圾要收集。在C ++中,智能指针有多种形式,最重要的是unique_ptr。unique_ptr本质上是一个单一的所有权和作用域构造。在设计良好的代码段中,大多数堆分配的内容通常将驻留在unique_ptr智能指针之后,并且将始终定义这些资源的所有权。unique_ptr几乎没有任何开销,并且unique_ptr消除了传统上将人们驱使到托管语言的大多数手动内存管理问题。现在,越来越多的并发运行内核变得越来越普遍,驱动代码在任何时间点使用唯一且定义明确的所有权的设计原则对性能变得越来越重要。

即使在设计良好的程序中,尤其是在多线程环境中,没有共享的数据结构也无法表达所有内容,对于真正需要的数据结构,线程需要进行通信。对于单线程设置中的生命周期问题,c ++中的RAII可以很好地发挥作用;在多线程设置中,对象的生命周期可能未完全按层次结构定义。对于这些情况,shared_ptr的使用提供了解决方案的很大一部分。您创建了资源的共享所有权,而在C ++中这是我们唯一看到的垃圾,但是数量如此之少,因此,与完全成熟的垃圾收集相比,应该更考虑设计适当的c ++程序来实现用shared-ptr进行“垃圾”收集,以其他语言实现。C ++根本没有那么多“垃圾”

正如其他人所述,引用计数的智能指针是垃圾收集的一种形式,而这有一个主要问题。该示例主要用作垃圾计数的引用计数形式的缺点,其中一个问题是创建孤立的数据结构,这些数据结构相互之间具有智能指针,从而创建了彼此不被收集的对象集群。在根据计算的参与者模型设计的程序中,当您使用广泛的共享数据方法进行多线程编程时,数据结构通常不允许在C ++中出现这种不可收集的簇,这主要是在很大程度上在整个行业中,这些孤立的集群可以迅速成为现实。

因此,总而言之,如果通过共享指针的使用,您的意思是与其他形式的垃圾回收相比,unique_ptr的广泛使用与用于多线程编程的计算方法的actor模型的结合以及对shared_ptr的有限使用,这比其他形式的垃圾回收毫无用处增加的好处。但是,如果采用一切共享的方法,最终到处都是shared_ptr,那么您应该考虑切换并发模型或切换到更适合于所有权的更广泛共享和对数据结构的并发访问的托管语言。


1
这是否意味着Rust不需要垃圾收集?
Gulshan

1
@Gulshan Rust是支持安全唯一指针的极少数语言之一。
CodesInChaos

2

大多数智能指针都是使用引用计数实现的。即,引用对象的每个智能指针都会增加对象引用计数。当该计数变为零时,将释放对象。

如果您有循环引用,就会出现问题。也就是说,A引用了B,B引用了C,C引用了A。如果您使用的是智能指针,则要释放与A,B和C关联的内存,您需要手动在循环引用中插入一个“中断”(例如weak_ptr在C ++中使用)。

垃圾收集(通常)的工作方式大不相同。如今,大多数垃圾收集器都使用可达性测试。也就是说,它看起来在所有堆栈上的引用是全局可访问的那些,然后跟踪每一个对象,这些引用是指,和对象,他们指,否则等一切都是垃圾。

这样,循环引用就不再重要了-只要A,B和C都不可达,就可以回收内存。

“真实的”垃圾回收还有其他优点。例如,内存分配非常便宜:只需将指针增加到内存块的“结尾”即可。解除分配也具有恒定的摊销成本。但是,当然,像C ++这样的语言可以让您以自己喜欢的任何方式实现内存管理,因此您可以提出更快的分配策略。

当然,在C ++中,堆分配的内存量通常少于像C#/。NET这样的重引用语言。但这并不是真正的垃圾回收与智能指针问题。

在任何情况下,问题都不是一劳永逸的。它们各有优缺点。


2

这与性能有关。取消分配内存需要大量管理。如果取消分配在后台运行,则前台进程的性能会提高。不幸的是,内存分配不能是懒惰的(分配的对象将在下一个神圣的时刻使用),但是释放对象可以。

尝试在C ++(不带任何GC)中分配一大堆对象,打印“ hello”,然后删除它们。您会惊讶于释放对象需要多长时间。

另外,GNU libc提供了更有效的工具来分配内存,请参阅obstacks。必须注意,我没有使用过堆叠的经验,我从未使用过。


从原则上讲,您有一点要指出,但是应该注意,这是一个非常简单的解决方案:使用池分配器或小型对象分配器来捆绑释放。但是,与在后台运行GC相比,这无疑需要(略)更多的努力。
康拉德·鲁道夫

是的,可以肯定,GC更舒适。(特别是对于初学者:没有所有权问题,甚至没有删除运算符。)
ern0 2010年

3
@ ern0:不 (引用计数)智能指针的全部要点是,没有所有权问题,也没有删除运算符。
康拉德·鲁道夫

3
@乔恩:说实话,大多数时候是这样。如果您愿意在不同线程之间共享对象状态,那么您将遇到完全不同的问题。我承认许多人都采用这种方式进行编程,但这是由于直到最近才存在的糟糕的线程抽象,这不是进行多线程的好方法。
康拉德·鲁道夫

1
解除分配通常不会在“后台”完成,而是会暂停所有前台线程。尽管暂停了前台线程,但批处理模式垃圾回收通常还是可以提高性能的,因为它可以合并未使用的空间。一个人可以将垃圾收集和堆压缩的过程分开,但是-尤其是在使用直接引用而不是句柄的框架中-它们都倾向于“停止世界”的过程,并且通常最实用一起。
超级猫

2

垃圾收集可以提高效率-基本上可以“分担”内存管理的开销,并且可以一次完成所有操作。通常,这将导致较少的总体CPU花费在内存分配上,但这意味着您在某个时候会有大量的分配活动。如果GC的设计不正确,则在GC尝试取消分配内存时,这可能会以“暂停”的形式显示给用户。除了在最不利的条件下,大多数现代GC都非常擅长使用户看不到它。

智能指针(或任何引用计数方案)的优势在于,它们恰好在您希望通过查看代码实现的时候发生(智能指针超出范围,事物被删除)。您到处都有少量的取消分配。总体而言,您在取消分配上可能会花费更多的CPU时间,但是由于它分散在程序中发生的所有事情上,因此用户看不到(禁止取消分配某些怪物数据结构)的可能性。

如果您正在做响应性很重要的事情,我建议智能指针/引用计数让您确切地知道什么时候发生,这样您就可以在编码的同时知道用户可能会看到的内容。在GC设置中,您对垃圾收集器只有最短暂的控制权,只需尝试解决该问题即可。

另一方面,如果您的目标是总体吞吐量,那么基于GC的系统可能会更好,因为它可以最大限度地减少执行内存管理所需的资源。

周期:我认为周期问题不是一个重大问题。在拥有智能指针的系统中,您倾向于没有周期的数据结构,或者只是对如何放开这些东西很小心。如有必要,可以使用知道如何打破所拥有对象中的循环的维护对象,以自动确保正确销毁。在某些编程领域中,这可能很重要,但是对于大多数日常工作而言,这是无关紧要的。


1
“在某个时候,您将有大量的取消分配活动”。贝克的跑步机就是一个完美的增量垃圾收集器。memorymanagement.org/glossary/t.html#treadmill
乔恩·哈罗普

1

智能指针的第一大限制是它们并不总是有助于避免循环引用。例如,您有一个对象A存储指向对象B的智能指针,而对象B存储一个指向对象A的智能指针。如果将它们放在一起而不重置任何一个指针,它们将永远不会被释放。

发生这种情况是因为智能指针必须执行特定的操作,在上述情况下,由于这两个对象都无法到达程序,因此不会触发该操作。垃圾收集将可以应对-它将正确识别程序无法访问的对象,并将对其进行收集。


1

这是光谱

如果您对性能没有严格的限制并且准备好投入使用,那么您将最终参加汇编或c大会,所有的责任都由您自己做出正确的决定以及这样做的所有自由,但是有了它,所有将其弄乱的自由:

“我会告诉你该怎么做,你要做。相信我”。

垃圾收集是另一端。您几乎没有控制权,但它会为您处理:

“我告诉你我想要什么,让它实现。”

这有很多优点,多数情况下,当您确切地知道何时不再需要资源时,您不必像以前那样值得信赖,但是(尽管这里有一些答案)对性能不利,并且性能的可预测性。(就像所有事情一样,如果您被赋予控制权并且做一些愚蠢的事情,您可能会得到更糟糕的结果。但是建议您在编译时知道能够释放内存的条件是,不能被认为是性能的胜利。天真无邪)。

RAII,作用域,引用计数都是使您沿着该频谱进一步前进的助手,但并非一直如此。所有这些事情仍然需要积极使用。它们仍然允许并要求您以垃圾回收所不能提供的方式与内存管理进行交互。


0

请记住,最后,一切归结为CPU执行指令。据我所知,所有消费级CPU都有指令集,这些指令集要求您将数据存储在内存中的给定位置,并且您必须具有指向该数据的指针。这就是您在基本级别上拥有的全部。

除了垃圾回收之外,所有可能已被移动的数据的引用,堆压缩等的工作都在上述“带有地址指针的内存块”范式所提供的限制内进行。智能指针也是如此-您仍然必须使代码在实际硬件上运行。

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.