it-swarm.cn

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

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

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

69
Gulshan

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

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

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

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

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

71
Jon Harrop

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

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

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

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

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

66
munificent

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

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

智能指针只是引用行为的任何结构,就像普通的指针一样,但附加了一些额外的功能。这些include,但不仅限于释放,还包括写时复制,绑定检查,...

现在,正如您所说的,智能指针可以使用实现垃圾收集的一种形式。

但是思路如下:

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

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

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

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

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

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

像C++这样的智能指针在像C#这样的语言中根本不可能,因为它们根本没有确定性的破坏(C#通过提供用于在某些对象上调用.Dispose()的语法糖来解决此问题)。 GC将回收未引用的资源最终,但确切时间将在未定义的资源中定义。

反过来,这可以使GC更有效率地完成工作。 .NET GC可以比设置在其上的智能指针更深入地构建在语言中。延迟内存操作并分块执行它们,以使它们更便宜,甚至move可以根据访问对象的频率提高存储效率。

36
Dario

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

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

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

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

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

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

4
Tom Anderson

术语垃圾收集意味着有任何垃圾要收集。在C++中,智能指针有多种形式,最重要的是unique_ptr。 unique_ptr本质上是一个单一的所有权和作用域构造。在设计良好的代码段中,大多数堆分配的内容通常将驻留在unique_ptr智能指针后面,并且将始终定义这些资源的所有权。 unique_ptr几乎没有任何开销,并且unique_ptr消除了传统上将人们驱使到托管语言的大多数手动内存管理问题。现在,越来越多的并发运行内核变得越来越普遍,驱动代码在任何时间点使用唯一且定义明确的所有权的设计原则对于性能而言变得越来越重要。使用actor计算模型可以构建线程之间共享状态量最少的程序,并且唯一所有权在使高性能系统有效利用许多内核而没有共享之间开销的过程中起着主要作用。线程数据和隐含的互斥要求。

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

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

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

4
user1703394

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

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

垃圾收集(通常)的工作方式大不相同。如今,大多数垃圾收集器都使用可达性测试。也就是说,它查看堆栈上的所有引用以及可全局访问的引用,然后跟踪这些引用所引用的每个对象以及对象they引用的对象,等等。其他所有东西都是垃圾。

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

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

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

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

2
Dean Harding

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

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

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

2
ern0

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

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

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

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

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

2
Michael Kohne

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

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

1
sharptooth

这是频谱

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

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

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

“我会告诉你我想要的,你实现了。”.

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

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

1
drjpizzle

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

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

0
user1249