垃圾收集在C + + – 为什么?
我不断听到有人抱怨C ++没有垃圾收集。 我还听说C ++标准委员会正在考虑将其添加到语言中。 我害怕我只是没有看到它的意义…使用RAII智能指针消除了它的需要,对不对?
我唯一的垃圾收集经验是在几台便宜的八十年代的家用电脑上,这意味着系统每隔几秒就会冻结几秒钟。 我相信自那时以来已经有所改善,但正如你所猜测的那样,并没有给我一个很高的评价。
垃圾收集为经验丰富的C ++开发人员提供了什么好处?
我不断听到有人抱怨C ++没有垃圾收集。
我为他们感到难过。 认真。
C ++有RAII,我总是抱怨在垃圾收集语言中找不到RAII(或阉割的RAII)。
垃圾收集为经验丰富的C ++开发人员提供了什么好处?
另一个工具。
Matt J在他的文章中写得很对( 在C ++中为垃圾收集 – 为什么? ):我们不需要C ++特性,因为它们中的大多数可以用C语言编写,而且我们不需要C特性,因为大多数特性都可以在Assembly中编码等.C ++必须进化。
作为开发人员:我不在乎GC。 我尝试了RAII和GC,我发现RAII非常优越。 正如Greg Rogers在他的文章( C ++中的垃圾收集 – 为什么? )中所说的那样,内存泄漏并不是很糟糕(至less在C ++中,如果真的使用C ++,那么它们很less),而不是RAII。 GC具有非确定性的释放/终止,只是一种编写代码而不关心具体的内存select的方法 。
最后一句很重要:编写“合理无关”的代码非常重要。 在C ++ RAII中,我们并不关心资源的释放,因为RAII是为我们做的,或者是因为构造函数为我们做对象初始化的,所以有时候编码时不需要关心谁是内存的所有者,和这个或这段代码所需要的什么样的指针(共享,弱等)。 似乎在C ++中需要GC。 (即使我个人没有看到它)
在C ++中使用GC的一个例子
有时,在一个应用程序中,你有“浮动数据”。 设想一下数据的树状结构,但是没有人真的是数据的“拥有者”(并且没有人真正关心它究竟会被销毁的时间)。 多个对象可以使用它,然后丢弃它。 你想在没有人使用它的时候释放它。
C ++的方法是使用智能指针。 boost :: shared_ptr浮现在脑海。 所以每一块数据都拥有自己的共享指针。 凉。 问题是当每一条数据都可以引用另一条数据。 您不能使用共享指针,因为它们使用的引用计数器不支持循环引用(A指向B,B指向A)。 所以你必须知道如何使用弱指针(boost :: weak_ptr)以及何时使用共享指针。
使用GC,您只需使用树结构化的数据。
缺点是你一定不在乎什么时候 “浮动数据”真的会被销毁。 只有它会被销毁。
结论
所以最后,如果做得很好,并且与当前的C ++成语相兼容,GC将成为C ++的另一个好工具 。
C ++是一个多语言的语言:添加一个GC可能会让一些C ++粉丝因叛国罪而哭泣,但最终可能是一个好主意,我猜C ++标准Comitee不会让这种主要function破坏语言,所以我们可以相信他们做了必要的工作来启用一个不会干扰C ++的正确的C ++ GC: 和C ++一样,如果你不需要一个特性,不要使用它,这会花费你没有。
简单的回答是垃圾收集与智能指针在原理上非常类似。 如果你分配的每一块内存位于一个对象内,并且这个对象只能被智能指针引用,那么你就有了一些接近于垃圾收集的东西(可能更好)。 这样做的好处在于不必对每个对象的范围和智能指针进行明智的判断,并让运行时为您做好工作。
这个问题似乎类似于“C ++为经验丰富的程序集开发人员提供了什么?指令和子程序消除了它的需要,对吗?
随着像valgrind这样的优秀内存检查工具的出现,我没有看到垃圾收集作为一个安全网的用处很大,“以防万一”我们忘记了释放某些东西 – 尤其是因为它并不能帮助pipe理更通用的资源除记忆之外(尽pipe这些都不太常见)。 另外,显式地分配和释放内存(即使是使用智能指针)在我看到的代码中也是非常罕见的,因为容器通常是更简单和更好的方式。
但是垃圾收集可能会提供潜在的性能优势,尤其是在分配了大量短期对象的情况下。 GC也可能为新创build的对象提供更好的引用位置(与堆栈中的对象相当)。
在C ++中GC支持的动机因素似乎是lambda编程,匿名函数等。事实certificate,lambda库受益于分配内存而不关心清理的能力。 对普通开发人员的好处是更简单,更可靠和更快地编译lambda库。
GC也有助于模拟无限的记忆; 您需要删除POD的唯一原因是您需要回收内存。 如果您有GC或无限的内存,则不需要再删除POD。
委员会不添加垃圾收集,他们添加了一些function,使垃圾收集更安全地实施。 只有时间才能说明它们是否对未来的编译器有任何影响。 具体的实现可能会有很大的差异,但很可能会涉及到基于可达性的收集,这可能会涉及一个小小的挂起,具体取决于如何完成。
但有一点是,没有符合标准的垃圾收集器将能够调用析构函数 – 只是默默地重用丢失的内存。
垃圾收集为经验丰富的C ++开发人员提供了什么好处?
无需追查经验不足的同事的代码中的资源泄漏。
我不明白怎么能说RAII取代GC,还是非常优越。 一个gc处理的很多情况是RAII完全无法处理的。 他们是不同的野兽。
首先,RAII不是防弹的:它可以防止在C ++中普遍存在的一些常见的失败,但是在很多情况下,RAII根本没有任何帮助; asynchronous事件(如UNIX下的信号)是脆弱的。 从根本上说,RAII依赖于范围:当一个variables超出范围,它会被自动释放(假设当然是正确实现了析构函数)。
下面是一个简单的例子,auto_ptr或RAII都不能帮助你:
#include <signal.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <memory> using namespace std; volatile sig_atomic_t got_sigint = 0; class A { public: A() { printf("ctor\n"); }; ~A() { printf("dtor\n"); }; }; void catch_sigint (int sig) { got_sigint = 1; } /* Emulate expensive computation */ void do_something() { sleep(3); } void handle_sigint() { printf("Caught SIGINT\n"); exit(EXIT_FAILURE); } int main (void) { A a; auto_ptr<A> aa(new A); signal(SIGINT, catch_sigint); while (1) { if (got_sigint == 0) { do_something(); } else { handle_sigint(); return -1; } } }
A的析构函数永远不会被调用。 当然,这是一个人为的,有点人为的例子,但实际上也可能发生类似的情况。 例如,当你的代码被另一个处理SIGINT的代码调用时,你根本无法控制(具体的例子:matlab中的mex扩展)。 这是为什么终于在python不保证执行的东西是相同的原因。 Gc在这种情况下可以帮助你。
其他成语在这方面performance不佳:在任何非平凡的程序中,你都需要有状态的对象(我在这里使用广义的对象这个词,它可以是这个语言所允许的任何结构)。 如果你需要控制一个函数之外的状态,你不能轻易的用RAII来实现(这就是为什么RAII对asynchronous编程没有帮助)。 OTOH,gc可以看到你的进程的整个内存,也就是知道它分配的所有对象,并且可以asynchronous清理。
使用gc的速度也会更快,原因相同:如果你需要分配/取消分配许多对象(尤其是小对象),除非你编写自定义的分配器,否则gc将大大超过RAII,因为gc可以分配/一次清理多个对象。 一些众所周知的C ++项目使用gc,即使在性能方面也是如此(例如Tim Sweenie关于在虚幻竞技场中使用gc的例子: http : //lambda-the-ultimate.org/node/1277 )。 GC基本上以延迟为代价增加吞吐量。
当然,有些情况下RAII比gc好; 特别是gc概念主要关注记忆,而这不是唯一的资源。 像文件等东西可以很好地处理与RAII。 没有像Python或ruby处理内存的语言在这些情况下有类似RAII的东西,BTW(在Python中有语句)。 当你精确地控制何时释放资源时,RAII是非常有用的,例如文件或锁常常是这种情况。
假设由于C ++没有将垃圾收集放入语言中 ,所以不能在C ++期间使用垃圾回收。 这是无稽之谈。 我知道那些使用Boehm收集器的精英C ++程序员当然是他们的工作。
垃圾收集可以推迟谁拥有一个对象的决定。
C ++使用值语义,所以对于RAII,事实上,当超出范围时,对象被重新收集。 这有时被称为“立即GC”。
当你的程序开始使用引用语义(通过智能指针等),语言不再支持你,你留给智能指针库的智慧。
关于GC的棘手的事情是决定什么时候不再需要一个对象。
垃圾收集使得RCU无锁同步更容易正确和高效地实现。
更简单的线程安全性和可扩展性
GC的一个属性在某些情况下可能非常重要。 指针的赋值在大多数平台上是自然primefaces的,而创build线程安全引用计数(“智能”)指针是相当困难的,并且引入了重要的同步开销。 因此,智能指针在多核架构上经常被告知“不能很好地扩展”。
垃圾收集实际上是自动资源pipe理的基础。 让GC以一种难以量化的方式改变你处理问题的方式。 例如,当您正在进行手动资源pipe理时,您需要:
- 考虑什么时候可以释放一个项目(是所有的模块/类完成它?)
- 考虑到在释放资源(准备释放资源的时候是哪个类/模块应该释放这个项目)的责任呢?
在微不足道的情况下,并不复杂。 例如,您在方法开始时打开一个文件,并在最后closures它。 或者调用者必须释放这个返回的内存块。
当你有多个与资源交互的模块,并且不清楚谁需要清理时,情况就会变得很复杂。 最终的结果是,解决问题的整个方法包括一些妥协的编程和devise模式。
在有垃圾收集的语言中,你可以使用一次性模式,在那里你可以释放你已经完成的资源,但是如果你没有释放它们,那么GC就是为了节省一天的时间。
智能指针,这实际上是我提到的妥协的一个完美的例子。 除非您有备份机制,否则智能指针不能防止泄漏循环数据结构。 为了避免这个问题,你经常妥协和避免使用循环结构,即使它可能是最合适的。
我也怀疑C ++ commitee是否在这个标准中增加了一个完整的垃圾回收机制。
但是我想说现代语言中join垃圾回收的主要原因是反垃圾回收的原因太less了。 自从八十年代以来,在内存pipe理和垃圾回收领域取得了一些巨大的进步,我相信甚至有垃圾收集策略可以给你类似软实时的保证(比如“GC不会超过.. ..在最坏的情况下“)。
使用RAII智能指针消除了它的需要,对不对?
智能指针可以用来在C ++中实现引用计数,这是一种垃圾收集(自动内存pipe理)的forms,但生产GC不再使用引用计数,因为它有一些重要的缺陷:
-
引用计数泄漏周期。 考虑A↔B,对象A和B都是相互引用的,所以它们都有一个引用计数为1,既不被收集也应该被收回。 像试用删除这样的高级algorithm解决了这个问题,但却增加了很多复杂性 使用
weak_ptr
作为解决方法正在回退到手动内存pipe理。 -
由于以下几个原因,天真的引用计数很慢。 首先,它需要经常使用超速caching引用计数(请参阅Boost的shared_ptr,速度比OCaml的垃圾收集慢10倍 )。 其次,在范围末尾注入的析构函数会招致不必要和昂贵的虚函数调用,并禁止诸如尾调用消除等优化。
-
基于范围的引用计数保持浮动垃圾,因为对象不会被循环到范围的末端,而追踪GC可以在它们变得不可达时立即回收它们,例如,在循环期间回收循环之前可以分配一个本地?
垃圾收集为经验丰富的C ++开发人员提供了什么好处?
生产力和可靠性是主要的好处。 对于许多应用程序,手动内存pipe理需要大量的程序员努力。 通过模拟一个无限存储机器,垃圾收集解放程序员从这个负担,使他们能够专注于解决问题,并逃避一些重要类别的错误(摇晃指针,缺lessfree
,双free
)。 此外,垃圾收集有利于其他forms的编程,例如通过解决向上的funarg问题(1970) 。
在支持GC的框架中,对像string这样的不可变对象的引用可能会像原语一样被传递。 考虑这个类(C#或Java):
public class MaximumItemFinder { String maxItemName = ""; int maxItemValue = -2147483647 - 1; public void AddAnother(int itemValue, String itemName) { if (itemValue >= maxItemValue) { maxItemValue = itemValue; maxItemName = itemName; } } public String getMaxItemName() { return maxItemName; } public int getMaxItemValue() { return maxItemValue; } }
请注意,这段代码从来不需要对任何string的内容做任何事情,而可以简单地把它们当作原语。 像maxItemName = itemName;
可能会产生两个指令:一个寄存器加载后跟一个寄存器存储。 MaximumItemFinder
将无法知道AddAnother
调用者是否将保留对传入string的任何引用,调用者将无法知道MaximumItemFinder
将保留多长时间引用它们。 getMaxItemName
将无法知道是否以及何时MaximumItemFinder
和返回的string的原始供应商已经放弃了对它的所有引用。 因为代码可以简单地像原始值一样传递string引用,但是, 这些都不重要 。
还要注意的是,虽然上面的类在同时调用AddAnother
的情况下不会是线程安全的,但是任何对GetMaxItemName
调用都将保证返回一个有效的引用,以传递给一个空string或一个已经传递给AddAnother
的string 。 如果需要确保最大项目名称和它的值之间的任何关系,则需要线程同步,但是即使在不存在的情况下也保证了内存安全性 。
我不认为有什么办法可以用C ++来编写上面的方法,它可以在任意multithreading的情况下维护内存的安全性,而不需要使用线程同步,也不需要每个stringvariables都有自己的内容副本,存放在其自己的存储空间中,在所涉及的variables的使用期限内可能不会被释放或重新定位。 当然不可能定义一个string引用types,这个types可以被定义,赋值,并像int
一样便宜地传递。
垃圾收集可以使您的最恶梦的泄漏
处理诸如循环引用之类事情的成熟的GC可能会比shared_ptr
有所升级。 我会用C ++来欢迎它,但不是在语言层面。
关于C ++的美女之一就是它不会强制垃圾回收。
我想纠正一个常见的误解:一个垃圾收集的神话 ,它以某种方式消除泄漏。 根据我的经验,debugging其他人编写代码的最糟糕的噩梦,并试图发现最昂贵的逻辑泄漏涉及通过资源密集型主机应用程序与embedded式Python等语言的垃圾回收。
在谈论GC这样的学科的时候,有理论,有实践。 理论上它是美好的,并防止泄漏。 然而在理论层面上,每种语言都是绝妙的,无泄漏的,因为从理论上讲,每个人都会写出完全正确的代码,并testing每一个单一代码可能出错的情况。
垃圾收集与不理想的团队协作相结合,造成我们案例中最严重,最难debugging的泄漏。
问题仍然与资源的所有权有关。 当涉及持久对象时,你必须在这里做出明确的devise决定,垃圾收集使得你很容易认为你不知道。
给定一些资源R
,在团队环境中,开发人员不是经常沟通和仔细审查对方的代码(在我的经验中有点太常见了),开发人员A
很容易将句柄那个资源。 开发者B
也可以以一种晦涩的方式来间接地将R
添加到某些数据结构中。 C
。 在垃圾收集系统中,这已经创build了3个R
所有者。
因为开发者A
是最初创build资源并认为他是所有者的开发者,所以他记得当用户表示不再使用它时,会释放对R
的引用。 毕竟,如果他不这样做,什么都不会发生,从testing中可以明显看出,用户端删除逻辑什么都不做。 所以他记得要释放它,因为任何合理的开发人员都会这样做。 这触发B
处理它的事件,并且还记得释放对R
的引用。
但是, C
忘了。 他不是这个团队中更强大的开发人员之一:一个刚刚在这个系统工作了一年的新人。 也许他甚至不在团队中,只是一个受欢迎的第三方开发者为我们的产品编写插件,许多用户添加到软件中。 随着垃圾收集,这是当我们得到那些沉默的逻辑资源泄漏。 它们是最糟糕的一种:除了程序运行时间过长,内存使用量持续上升以及某些神秘目的之外,它们并不一定在软件的用户可见面显示为明显的错误。 试图用debugging器来缩小这些问题可以和debugging时间敏感的竞争条件一样有趣。
没有垃圾收集,开发人员C
会创build一个悬挂指针 。 他可能会尝试访问它,导致软件崩溃。 现在这是一个testing/用户可见的错误。 C
尴尬了一下,并纠正了他的错误。 在GC场景中,试图找出系统泄漏的位置可能非常困难,以至于某些泄漏不会被纠正。 这些不是valgrind
types的物理泄漏,可以很容易地检测到,并精确到一个特定的代码行。
随着垃圾收集,开发人员C
创造了一个非常神秘的泄漏。 他的代码可能会继续访问R
, R
现在只是软件中的一个不可见的实体,与用户在这一点上无关,但仍处于有效状态。 而且随着C
的代码创build更多的泄漏,他在不相干的资源上创build了更多隐藏的处理,而且这个软件不仅泄漏内存,而且每次都变得越来越慢。
所以垃圾收集不一定能减轻逻辑资源的泄漏。 它可以在不太理想的情况下,使漏洞更容易被默默无知地留在软件中。 开发人员可能会非常沮丧地试图追查他们的GC逻辑泄漏,他们只是告诉用户定期重启软件作为一种解决方法。 它确实消除了悬挂指针,而在一个安全的软件中,在任何情况下崩溃都是完全不可接受的,所以我更喜欢GC。 但是我经常在不那么安全的环境中工作,但资源密集,性能关键的产品,可以立即修复的崩溃优于一个非常模糊和神秘的静默错误,资源泄漏并不是微不足道的错误。
在这两种情况下,我们都在讨论不在堆栈中的持久对象,比如3D软件中的场景图,合成器中提供的video剪辑或游戏世界中的敌人。 当资源与生命周期相关时,C ++和任何其他GC语言都会使得正确pipe理资源变得微不足道。 真正的困难在于不断引用其他资源的资源。
在C或C ++中,如果您无法清楚地指定谁拥有资源以及何时应该释放对它们的句柄(例如:设置为null以响应事件),则可以使用由段错误导致的悬挂指针和崩溃。 然而,在GC中,这个响亮而令人讨厌的但常常易于发现的崩溃被交换为无法检测到的无声资源泄漏。