如今,垃圾收集了许多语言。第三方甚至可以使用C ++。但是C ++具有RAII和智能指针。那么使用垃圾回收有什么意义呢?它在做额外的事情吗?
在其他语言(例如C#)中,如果所有引用都被视为智能指针(不考虑RAII),那么按照规范和实现,是否还会需要垃圾收集器?如果否,那为什么不是呢?
如今,垃圾收集了许多语言。第三方甚至可以使用C ++。但是C ++具有RAII和智能指针。那么使用垃圾回收有什么意义呢?它在做额外的事情吗?
在其他语言(例如C#)中,如果所有引用都被视为智能指针(不考虑RAII),那么按照规范和实现,是否还会需要垃圾收集器?如果否,那为什么不是呢?
Answers:
那么,使用垃圾回收有什么意义呢?
我假设您的意思是引用计数的智能指针,并且我会注意到它们是(基本的)垃圾收集形式,因此我将回答“其他形式的垃圾收集相对于引用计数的智能指针有何优势”的问题代替。
准确性。引用计数本身会泄漏周期,因此,除非添加其他技术来捕获周期,否则引用计数的智能指针通常会泄漏内存。一旦添加了这些技术,引用计数的简单性优势便消失了。另外,请注意,基于范围的引用计数和跟踪GC在不同时间收集值,有时引用计数收集较早,而有时跟踪GC收集较早。
吞吐量。智能指针是垃圾收集效率最低的形式之一,尤其是在多线程应用程序中,引用计数被原子地增加时,尤其如此。有一些旨在减轻这种情况的高级参考计数技术,但跟踪GC仍然是生产环境中的首选算法。
延迟时间。典型的智能指针实现使析构函数发生雪崩,从而导致无限的暂停时间。其他形式的垃圾收集更具增量性,甚至可以是实时的,例如贝克的跑步机。
既然没有人从这个角度看过它,那么我将重新表述您的问题:如果可以在库中进行操作,为什么还要在语言中添加一些内容?忽略特定的实现和语法细节,GC /智能指针基本上是该问题的特例。如果可以在库中实现垃圾收集器,为什么还要用语言本身定义垃圾收集器?
这个问题有两个答案。最重要的第一:
您确保所有代码都可以使用它进行互操作。我认为,这是直到Java / C#/ Python / Ruby才真正实现代码重用和代码共享的重要原因。库需要进行通信,并且它们唯一可靠的共享语言就是语言规范本身(在某种程度上还包括其标准库)中的内容。如果您曾经尝试过在C ++中重用库,则可能会遇到标准内存语义没有引起的可怕痛苦。我想将结构传递给某个lib。我可以通过参考吗?指针?scoped_ptr
?smart_ptr
?我是否通过所有权?有没有办法表明这一点?如果库需要分配怎么办?我必须给它分配器吗?通过不使内存管理成为语言的一部分,C ++迫使每对库都必须在这里协商自己的特定策略,而且很难让所有人都同意。GC保证完全不发行。
您可以围绕它设计语法。因为C ++本身并不封装内存管理,所以它必须提供一系列语法挂钩,以使用户级代码可以表达所有细节。您有指针,引用,,const
解引用运算符,间接运算符,地址等。如果将内存管理引入语言本身,则可以围绕该语言设计语法。所有这些运算符都消失了,并且语言变得更简洁了。
您可以获得很高的投资回报率。给定代码段生成的值乘以使用它的人数。这意味着您拥有的用户越多,您花得更多的钱可以花在一件软件上。将功能移入语言后,该语言的所有用户都将使用它。这意味着您可以比仅分配给那些用户的一部分使用的库分配更多的精力。这就是为什么Java和C#之类的语言拥有绝对一流的VM和极好的高质量垃圾收集器的原因:开发它们的成本分摊到数百万用户中。
Dispose
封装了位图的对象,则对该对象的任何引用将是对已处置位图对象的引用。如果在其他代码仍希望使用该对象的情况下过早删除该对象,则位图类可以确保其他代码将以可预测的方式失败。相比之下,使用对已释放内存的引用是未定义行为。
垃圾回收基本上只是意味着您分配的对象在无法访问之后的某个时候自动释放。
更准确地说,它们在程序无法访问时被释放,因为循环引用的对象永远不会被释放。
智能指针仅指行为类似于普通指针但具有一些附加功能的任何结构。这些包括但不限于解除分配,还包括写时复制,绑定检查,...
现在,如您所述,智能指针可用于实现垃圾收集的一种形式。
但是思路如下:
当然,您可以从一开始就设计它。C#被设计为垃圾回收的,因此只有new
您的对象,并且在引用超出范围时将被释放。如何完成此操作取决于编译器。
但是在C ++中,没有垃圾回收的意图。如果我们分配了一些指针int* p = new int;
,但它不在范围之内,p
则会将自身从堆栈中删除,但是没有人会照顾分配的内存。
现在,您从一开始就只有确定性析构函数。当对象离开其创建范围时,将调用其析构函数。结合模板和运算符重载,您可以设计一个包装器对象,其行为类似于指针,但是使用析构函数来清理附加到其上的资源(RAII)。您将此称为智能指针。
这都是高度C ++特定的:运算符重载,模板,析构函数,...在这种特定的语言情况下,您已经开发了智能指针来为您提供所需的GC。
但是,如果您从一开始就使用GC设计语言,那么这仅仅是实现细节。您只是说对象将被清除,编译器将为您完成此操作。
像C ++这样的智能指针在像C#这样的语言中甚至不可能实现,而C#根本没有确定性的破坏(C#通过提供用于调用.Dispose()
某些对象的语法糖来解决此问题)。未引用的资源最终将由GC回收,但是确切何时会发生未定义的资源。
反过来,这可以使GC更有效率地完成工作。.NET GC的语言比设置在其之上的智能指针更深入地构建在语言中,例如,可以延迟内存操作并分块执行它们,以使其更便宜,甚至可以移动内存以提高效率,具体取决于对象的使用频率被访问。
IDisposable
和确实具有确定性破坏的形式using
。但这需要程序员一点点的努力,这就是为什么它通常仅用于非常稀缺的资源(例如数据库连接句柄)的原因。
IDisposable
通过将常规替换let ident = value
为use ident = value
... 来尝试更简单的语法...
using
与垃圾回收完全无关,它只是在变量超出范围时才调用函数,就像C ++中的析构函数一样。
我认为,垃圾回收和用于内存管理的智能指针之间有两个大区别:
前者意味着GC将收集智能指针不会收集的垃圾。如果您使用的是智能指针,则必须避免创建此类垃圾,或者准备手动进行处理。
后者意味着无论智能指针多么聪明,它们的操作都会减慢程序中的工作线程。垃圾回收可以延迟工作,并将其移至其他线程;这样就可以提高整体效率(实际上,即使没有智能指针的额外开销,现代GC的运行时成本也比普通的malloc / free系统要少),并且无需花费大量时间即可完成它仍需要做的工作应用程序线程的方式。
现在,请注意,作为程序构造的智能指针可用于执行各种其他有趣的操作-参见Dario的答案-完全超出了垃圾回收的范围。如果要执行这些操作,则需要智能指针。
但是,出于内存管理的目的,我看不到智能指针取代垃圾回收的任何前景。他们根本不那么擅长。
using
在后续C#版本中引入了该块。此外,在实时系统中可能会禁止GC的不确定行为(这就是为什么不在那里使用GC的原因)。另外,不要忘了GC 太复杂而无法正确处理,以至于实际上大多数内存泄漏并且效率很低(例如Boehm…)。
术语垃圾收集意味着有任何垃圾要收集。在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,那么您应该考虑切换并发模型或切换到更适合于所有权的更广泛共享和对数据结构的并发访问的托管语言。
Rust
不需要垃圾收集?
大多数智能指针都是使用引用计数实现的。即,引用对象的每个智能指针都会增加对象引用计数。当该计数变为零时,将释放对象。
如果您有循环引用,就会出现问题。也就是说,A引用了B,B引用了C,C引用了A。如果您使用的是智能指针,则要释放与A,B和C关联的内存,您需要手动在循环引用中插入一个“中断”(例如weak_ptr
在C ++中使用)。
垃圾收集(通常)的工作方式大不相同。如今,大多数垃圾收集器都使用可达性测试。也就是说,它看起来在所有堆栈上的引用是全局可访问的那些,然后跟踪每一个对象,这些引用是指,和对象,他们指,否则等一切都是垃圾。
这样,循环引用就不再重要了-只要A,B和C都不可达,就可以回收内存。
“真实的”垃圾回收还有其他优点。例如,内存分配非常便宜:只需将指针增加到内存块的“结尾”即可。解除分配也具有恒定的摊销成本。但是,当然,像C ++这样的语言可以让您以自己喜欢的任何方式实现内存管理,因此您可以提出更快的分配策略。
当然,在C ++中,堆分配的内存量通常少于像C#/。NET这样的重引用语言。但这并不是真正的垃圾回收与智能指针问题。
在任何情况下,问题都不是一劳永逸的。它们各有优缺点。
这与性能有关。取消分配内存需要大量管理。如果取消分配在后台运行,则前台进程的性能会提高。不幸的是,内存分配不能是懒惰的(分配的对象将在下一个神圣的时刻使用),但是释放对象可以。
尝试在C ++(不带任何GC)中分配一大堆对象,打印“ hello”,然后删除它们。您会惊讶于释放对象需要多长时间。
另外,GNU libc提供了更有效的工具来分配内存,请参阅obstacks。必须注意,我没有使用过堆叠的经验,我从未使用过。
垃圾收集可以提高效率-基本上可以“分担”内存管理的开销,并且可以一次完成所有操作。通常,这将导致较少的总体CPU花费在内存分配上,但这意味着您在某个时候会有大量的分配活动。如果GC的设计不正确,则在GC尝试取消分配内存时,这可能会以“暂停”的形式显示给用户。除了在最不利的条件下,大多数现代GC都非常擅长使用户看不到它。
智能指针(或任何引用计数方案)的优势在于,它们恰好在您希望通过查看代码实现的时候发生(智能指针超出范围,事物被删除)。您到处都有少量的取消分配。总体而言,您在取消分配上可能会花费更多的CPU时间,但是由于它分散在程序中发生的所有事情上,因此用户看不到(禁止取消分配某些怪物数据结构)的可能性。
如果您正在做响应性很重要的事情,我建议智能指针/引用计数让您确切地知道什么时候发生,这样您就可以在编码的同时知道用户可能会看到的内容。在GC设置中,您对垃圾收集器只有最短暂的控制权,只需尝试解决该问题即可。
另一方面,如果您的目标是总体吞吐量,那么基于GC的系统可能会更好,因为它可以最大限度地减少执行内存管理所需的资源。
周期:我认为周期问题不是一个重大问题。在拥有智能指针的系统中,您倾向于没有周期的数据结构,或者只是对如何放开这些东西很小心。如有必要,可以使用知道如何打破所拥有对象中的循环的维护对象,以自动确保正确销毁。在某些编程领域中,这可能很重要,但是对于大多数日常工作而言,这是无关紧要的。
这是光谱。
如果您对性能没有严格的限制并且准备好投入使用,那么您将最终参加汇编或c大会,所有的责任都由您自己做出正确的决定以及这样做的所有自由,但是有了它,所有将其弄乱的自由:
“我会告诉你该怎么做,你要做。相信我”。
垃圾收集是另一端。您几乎没有控制权,但它会为您处理:
“我告诉你我想要什么,让它实现。”
这有很多优点,多数情况下,当您确切地知道何时不再需要资源时,您不必像以前那样值得信赖,但是(尽管这里有一些答案)对性能不利,并且性能的可预测性。(就像所有事情一样,如果您被赋予控制权并且做一些愚蠢的事情,您可能会得到更糟糕的结果。但是建议您在编译时知道能够释放内存的条件是,不能被认为是性能的胜利。天真无邪)。
RAII,作用域,引用计数等都是使您沿着该频谱进一步前进的助手,但并非一直如此。所有这些事情仍然需要积极使用。它们仍然允许并要求您以垃圾回收所不能提供的方式与内存管理进行交互。
请记住,最后,一切归结为CPU执行指令。据我所知,所有消费级CPU都有指令集,这些指令集要求您将数据存储在内存中的给定位置,并且您必须具有指向该数据的指针。这就是您在基本级别上拥有的全部。
除了垃圾回收之外,所有可能已被移动的数据的引用,堆压缩等的工作都在上述“带有地址指针的内存块”范式所提供的限制内进行。智能指针也是如此-您仍然必须使代码在实际硬件上运行。