为什么C ++没有垃圾收集器?
首先,我不是在问这个问题,因为垃圾回收的优点。 我问这个问题的主要原因是我知道Bjarne Stroustrup曾经说C ++在某个时间点上会有一个垃圾收集器。
这就是说,为什么没有添加? C ++已经有一些垃圾收集器了。 这只是其中一个“容易说什么”types的东西? 还是还有其他的原因没有被添加(并且不会被添加到C ++ 11中)?
交叉链接:
- 垃圾收集器的C + +
为了澄清,我明白了C ++在第一次创build时没有垃圾回收器的原因。 我想知道为什么collections家不能被添加进去。
隐式垃圾收集可能已经被添加进去了,但是它并没有完成。 可能是由于不仅仅是执行复杂性,还有可能是由于人们无法足够快速地达成共识。
Bjarne Stroustrup本人的一句名言:
我曾希望可以select启用的垃圾收集器是C ++ 0x的一部分,但是我有足够的技术问题,只能详细说明这样的收集器如何与其他语言集成,如果提供。 与基本上所有C ++ 0xfunction的情况一样,存在一个实验性实现。
这里有一个很好的讨论这个话题。
总体概述:
C ++非常强大,可以让你做任何事情。 出于这个原因,它不会自动将许多事情推到你的身上,这可能会影响性能。 垃圾收集可以很容易地通过智能指针来实现(对象的引用计数为指针,当引用计数达到0时自动删除它们)。
C ++是build立在竞争者的脑海里,没有垃圾收集。 与C和其他人相比,效率是C ++必须避开批评的主要关注点。
有两种types的垃圾收集…
显式垃圾收集:
C ++ 0x将通过使用shared_ptr创build的指针进行垃圾回收
如果你想要它,你可以使用它,如果你不想要它,你不会被迫使用它。
如果您不想等待C ++ 0x,那么您当前也可以使用boost:shared_ptr。
隐式垃圾收集:
它虽然没有透明的垃圾收集。 这将成为未来C ++规范的焦点。
为什么Tr1没有隐式垃圾收集?
C ++ 0x的tr1应该有很多东西,Bjarne Stroustrup在之前的采访中表示,tr1没有他所希望的那么多。
在这里join辩论。
垃圾收集存在已知的问题,理解它们有助于理解C ++中为什么没有。
1.性能?
第一个投诉往往是关于performance,但大多数人并没有真正意识到他们在谈论什么。 正如Martin Beckett
( Martin Beckett
所说明的那样,问题本身可能并不是performance,而是performance的可预测性。
目前有2个GC系列被广泛部署:
- 马克 – 扫 – 样
- 参考 – 计数种类
Mark And Sweep
速度更快(对整体性能的影响更小),但是却遭受了“冻结世界”的混乱:即当GC启动时,其他所有事情都停止,直到GC完成清理。 如果你想build立一个服务器,在几毫秒内回答…一些交易不会达到你的期望:)
Reference Counting
的问题是不同的:引用计数会增加开销,尤其是在multithreading环境中,因为您需要有一个primefaces计数。 此外还有参考周期的问题,所以你需要一个聪明的algorithm来检测这些周期并消除它们(通常也是通过“冻结世界”来实现,尽pipe不太频繁)。 一般来说,从今天起,这种types(即使通常比较敏感,或者更不常见),比Mark And Sweep
慢。
我看到一个由埃菲尔实施者正在试图实施一个Reference Counting
垃圾收集器,将有一个类似的全球性能Mark And Sweep
没有“冻结世界”方面的文件。 它需要一个单独的GC(典型)线程。 该algorithm有点可怕(最后),但是本文很好地介绍了这些概念,并展示了algorithm从“简单”版本发展到完整版本的过程。 推荐阅读,只要我可以把我的手放回PDF文件…
2.资源获取是初始化
在C++
,这是一个常见的习惯用法,您将包装对象中的资源的所有权,以确保它们被正确释放。 它主要用于内存,因为我们没有垃圾收集,但是对于许多其他情况也是有用的:
- 锁(multithreading,文件句柄,…)
- 连接(到数据库,另一个服务器…)
这个想法是适当地控制对象的生命周期:
- 只要你需要,它应该是活着的
- 它应该被杀死,当你完成它
GC的问题是,如果它对前者有帮助,并最终保证以后…这个“终极”可能是不够的。 如果你释放一个锁,你真的希望它现在被释放,所以它不会阻止任何进一步的呼叫!
GC的语言有两个解决方法:
- 当堆栈分配足够时,不要使用GC:这通常是性能问题,但在我们的情况下,它确实有帮助,因为范围定义了生命周期
-
using
构造…但它是显式(弱)RAII,而在C ++ RAII是隐式的,这样用户不会无意中造成错误(通过省略using
关键字)
3.智能指针
智能指针经常以C++
处理内存的方式出现。 我经常听到:毕竟我们不需要GC,因为我们有聪明的指针。
一个不可能是更错误的。
智能指针确实有帮助: auto_ptr
和unique_ptr
使用RAII概念,确实非常有用。 它们非常简单,你可以很容易地自己写。
当一个人需要分享所有权的时候,它会变得更加困难:你可能会在多个线程之间共享,并且在处理计数时有一些细微的问题。 因此,一个自然而然的去往shared_ptr
。
这真是太好了,毕竟这是提振,但不是银弹。 实际上, shared_ptr
的主要问题在于它模拟了Reference Counting
实现的GC,但是您需要自己实现周期检测… Urg
当然,这是weak_ptr
thingy,但是不幸的是,由于这些循环,尽pipe使用了shared_ptr
,已经看到了内存泄漏,而且当您处于multithreading环境时,检测非常困难!
4.什么是解决scheme?
没有银弹,但一如既往,这绝对是可行的。 在没有GC的情况下,需要明确所有权:
- 如果可能的话,更喜欢在一个特定的时间拥有一个所有者
- 如果没有,请确保您的类图没有任何有关所有权的周期,并用
weak_ptr
微妙应用程序来分解它们
所以确实有一个GC是很棒的,但这不是一个小问题。 与此同时,我们只需要卷起袖子。
什么样的? 是否应该针对embedded式洗衣机控制器,手机,工作站或超级计算机进行优化?
应该优先考虑GUI响应还是服务器负载?
应该使用大量的内存还是大量的CPU?
C / c ++在很多不同的情况下使用。 我怀疑像大多数用户提升智能指针就足够了
编辑 – 自动垃圾收集器不是一个性能问题(你总是可以购买更多的服务器)这是一个可预测性能的问题。
GC不知道什么时候开始,就像雇用一个发火车的飞行员一样,大部分时间他们都很棒 – 但是当你真的需要响应的时候!
C ++没有内build垃圾回收的最大原因之一是让垃圾收集与析构函数搭配起来真的非常困难。 据我所知,没有人真正知道如何完全解决它。 有很多问题需要处理:
- 对象的确定性生存期(引用计数给你这个,但是GC没有,虽然可能不是那么重要)。
- 如果一个析构函数抛出对象被垃圾收集时会发生什么? 大多数语言都忽略了这个exception,因为它们确实没有catch块能够传输,但这可能不是C ++的可接受的解决scheme。
- 如何启用/禁用它? 当然,这可能是一个编译时间的决定,但是为GC编写的代码与为非GC编写的代码将会非常不同,并且可能不兼容。 你如何协调这一点?
这些只是一些面临的问题。
尽pipe这是一个老问题,但仍然有一个问题我根本没有看到任何人解决:垃圾收集几乎是不可能指定的。
特别是,C ++标准非常小心地用外部可观察的行为来指定语言,而不是实现如何实现这种行为。 然而,在垃圾收集的情况下,几乎没有外部可观察到的行为。
垃圾收集的总体思路是要合理地确保内存分配的成功。 不幸的是,即使你有一个垃圾收集器正在运行,保证任何内存分配都将成功,这实际上是不可能的。 这在某种程度上是正确的,但在C ++的情况下尤其如此,因为它可能(可能)不可能使用在收集周期中移动内存中的对象的复制收集器(或类似的东西)。
如果你不能移动对象,你不能创build一个单独的,连续的内存空间来做你的分配 – 这意味着你的堆(或自由存储,或任何你喜欢称之为)可以,也可能会,随着时间的推移变得碎片化。 这反过来也可以防止分配成功,即使有更多的内存空间比请求的数量。
虽然有可能提出一些保证(实质上)说,如果重复完全相同的分配模式,并且第一次成功,它将在后续迭代中继续成功,前提是分配的内存在迭代之间变得不可访问。 这是一个很弱的保证,它本质上是无用的,但我看不出有任何加强的合理希望。
即使如此,它比C ++提出的要强。 以前的build议 [警告:PDF](被丢弃)根本不能保证。 在28页的提案中,你从外部观察到的行为方式是单一的(非规范性的)注释:
[注意:对于垃圾收集程序,高质量的托pipe实现应该尝试最大化其回收的不可访问的内存量。 – 注意]
至less对我而言,这提出了一个关于投资回报的严重问题。 我们打算打破现有的代码(没有人确定究竟有多less,但肯定是相当多的),对实现提出了新的要求,并对代码进行了新的限制,而我们得到的回报完全可能是什么都没有?
即使在最好的情况下,我们得到的是基于Javatesting的程序 ,可能需要大约六倍于现在以相同速度运行的内存。 更糟糕的是,垃圾收集从一开始就是Java的一部分 – C ++对垃圾收集器提出了更多限制,几乎肯定会有更差的成本/收益比(即使我们超出了build议的保证范围,一些好处)。
我从math上总结了这种情况:这是一个复杂的情况。 正如任何math家所知道的,复数有两部分:真实的和想象的。 在我看来,我们在这里所拥有的成本是真实的,但是(至less大部分)是虚构的。
如果你想自动垃圾收集,C ++有很好的商业和公共领域垃圾收集器。 对于垃圾收集合适的应用程序来说,C ++是一种优秀的垃圾收集语言,其性能与其他垃圾收集语言相比有优势。 有关C ++中的自动垃圾收集的讨论,请参阅C ++编程语言(第3版)。 另见Hans-J。 Boehm的C和C ++垃圾收集站点。 而且,C ++支持编程技术,允许内存pipe理在没有垃圾收集器的情况下是安全的和隐含的。
资料来源: http : //www.stroustrup.com/bs_faq.html#garbage-collection
至于为什么它没有内置,如果我没有记错的话,它是在GC之前发明的 ,我不相信这个语言可能有几个原因(IE向后兼容C)
希望这可以帮助。
C ++背后的想法是,您不会为不使用的function支付任何性能影响。 因此,添加垃圾收集意味着一些程序直接在硬件上运行,例如C和一些运行时虚拟机。
没有什么能阻止你使用绑定到某些第三方垃圾收集机制的某种forms的智能指针。 我似乎回想起微软正在用COM做这样的事情,但是它做得并不好。
Stroustrup在2013 Going Native会议上对此做了一些很好的评论。
在这个video中跳到25米50左右。 (我build议实际观看整个video,但是这会跳到关于垃圾收集的东西。)
当你有一个非常好的语言,可以很容易(和安全,可预测,易于阅读,易于教学)直接处理对象和值,避免(明确)使用堆,那么你甚至不想垃圾收集。
使用现代的C ++和C ++ 11中的东西,除了在有限的情况下垃圾收集不再是可取的。 事实上,即使一个好的垃圾收集器被内置到一个主要的C ++编译器中,我认为它也不会经常被使用。 避免GC会更容易 ,不难。
他展示了这个例子:
void f(int n, int x) { Gadget *p = new Gadget{n}; if(x<100) throw SomeException{}; if(x<200) return; delete p; }
这在C ++中是不安全的。 但是这在Java中也是不安全的! 在C ++中,如果函数返回的时间早, delete
将永远不会被调用。 但是,如果你有完整的垃圾收集,比如在Java中,你只会得到一个build议:“在将来的某个时候”这个对象会被破坏( 更新:更糟糕的是,Java并不保证会调用终结器 – 它可能永远不会被称为)。 如果Gadget持有一个打开的文件句柄,或者连接到一个数据库,或者稍后为了写入数据库而缓冲的数据,这样做还不够好。 我们希望小工具一旦完成就被销毁,以便尽快释放这些资源。 您不希望您的数据库服务器与数千个不再需要的数据库连接纠缠在一起 – 它不知道您的程序已经完成工作。
那么解决scheme是什么? 有几种方法。 您将用于绝大多数对象的显而易见的方法是:
void f(int n, int x) { Gadget p = {n}; // Just leave it on the stack (where it belongs!) if(x<100) throw SomeException{}; if(x<200) return; }
这需要input更less的字符。 它没有new
的方式。 它不需要您input两次Gadget
。 该对象在函数结束时被销毁。 如果这是你想要的,这是非常直观的。 Gadget
的行为与int
或double
相同。 可预测,易于阅读,易于教授。 一切都是“价值”。 有时候是一个很大的价值,但是价值观更容易教导,因为你没有这个“指针”(或引用)的“距离”。
你所做的大部分对象只能用在创build它们的函数中,并且可能作为子函数的input。 程序员不应该在返回对象时考虑“内存pipe理”,或者在软件的相互分开的部分共享对象。
范围和寿命是重要的。 大多数情况下,如果生命周期与范围相同,则更容易。 这是更容易理解,更容易教。 当你想要一个不同的生命周期时,应该很明显地阅读你正在做这个的代码,例如使用shared_ptr
。 (或者通过值返回(大)对象,利用move-semantics或unique_ptr
。
这可能看起来像是一个效率问题。 如果我想从foo()
返回一个小工具怎么办? C ++ 11的移动语义使得返回大对象变得更容易。 只需编写Gadget foo() { ... }
,它就会工作,并迅速工作。 你不需要和自己混在一起,只要按照价值归还东西,而且语言通常可以进行必要的优化。 (甚至在C ++ 03之前,编译器在避免不必要的复制方面做得非常好。)
正如Stroustrup在video中的其他地方所说: “只有一个计算机科学家会坚持复制一个物体,然后摧毁原来的(观众笑)为什么不把物体直接移动到新的位置呢? (而不是计算机科学家)期望的。“
当你可以保证只需要一个对象的副本时,就很容易理解对象的生命周期。 你可以select你想要的生命周期策略,如果你想要垃圾回收。 但是当你理解其他方法的好处时,你会发现垃圾回收处于你的首选项列表的底部。
如果这不适合你,你可以使用unique_ptr
,或者失败的, shared_ptr
。 编写得很好的C ++ 11比内存pipe理的其他语言更短,更容易阅读,也更易于教学。
要回答关于C ++的大多数“为什么”的问题,请阅读C ++的devise和演化
所有的技术性谈话都让这个概念过于复杂。
如果你将GC自动放入所有内存的C ++,那么考虑一下类似于Web浏览器的东西。 Web浏览器必须加载完整的Web文档并运行Web脚本。 您可以将Web脚本variables存储在文档树中。 在打开大量选项卡的浏览器中的BIG文档中,这意味着每次GC必须执行完整的收集时,还必须扫描所有文档元素。
在大多数电脑上,这意味着会发生页面故障。 所以回答这个问题的主要原因是会出现PAGE FAULTS。 当您的PC开始进行大量磁盘访问时,您将会知道这一点。 这是因为GC必须接触很多内存才能certificate无效的指针。 当你有一个真正的应用程序使用大量的内存,必须扫描所有对象的每个集合是严重的,因为页面错误。 页面错误是当虚拟内存需要从磁盘读回RAM时。
所以正确的解决scheme是将应用程序划分为需要GC的部分和不需要的部分。 在上面的web浏览器例子的情况下,如果文档树是用malloc分配的,但是JavaScript是用GC运行的,那么GC每次启动时只扫描一小部分内存和所有内存的PAGED OUT元素文档树不需要重新分页。
为了进一步理解这个问题,查看虚拟内存以及它是如何在计算机上实现的。 事实上,当没有真正的RAM时,2GB可用于程序。 对于具有2GB RAM的32Bit系统的现代计算机来说,只有一个程序正在运行就不是这样的问题。
作为一个附加的例子,考虑一个必须跟踪所有对象的完整集合。 首先,您必须扫描所有可通过根访问的对象。 第二步扫描在步骤1中可见的所有对象。然后扫描等待析构函数。 然后再次进入所有的页面,并closures所有不可见的对象。 这意味着许多页面可能被多次换出并返回。
所以我简短的回答是,由于触摸所有内存而导致页面错误的数量会导致程序中所有对象的完整GC不可行,所以程序员必须将GC视为脚本之类的辅助工具和数据库的工作,但手动内存pipe理做正常的事情。
另一个非常重要的原因当然是全局variables。 为了让收集器知道全局variables指针在GC中,它需要特定的关键字,因此现有的C ++代码将无法工作。
简短的回答:我们不知道如何有效地进行垃圾回收(时间和空间都很小),并且始终正确(在所有可能的情况下)。
很长的答案:就像C一样,C ++是一种系统语言, 这意味着它在您编写系统代码(例如操作系统)时使用。 换句话说,就像C一样,C ++被devise为具有最佳性能的主要目标。 语言标准不会添加任何可能阻碍性能目标的function。
这暂停了这个问题:为什么垃圾收集会阻碍性能? The main reason is that, when it comes to implementation, we [computer scientists] do not know how to do garbage collection with minimal overhead, for all cases. Hence it's impossible to the C++ compiler and runtime system to perform garbage collection efficiently all the time. On the other hand, a C++ programmer, should know his design/implementation and he's the best person to decide how to best do the garbage collection.
Last, if control (hardware, details, etc.) and performance (time, space, power, etc.) are not the main constraints, then C++ is not the write tool. Other language might serve better and offer more [hidden] runtime management, with the necessary overhead.
When you compare C++ with Java, you can see immediately that C++ was not designed with implicit Garbage Collection in mind, while Java was.
Having things like arbitrary pointers in C-Style and deterministic destructors does not only slow down the performance of GC-implementations, it would also destroy backward compatibility for a large amount of C++-legacy-code.
In addition to that, C++ is a language that is intended to run as standalone executable instead of having a complex run-time environment.
All in all: Yes it would be possible to add Garbage Collection to C++, but for the sake of continuity it is better not to do so. The cost of doing so would be greater than the benefit.
One of the fundamental principles behind the original C language is that memory is composed of a sequence of bytes, and code need only care about what those bytes mean at the exact moment that they are being used. Modern C allows compilers to impose additional restrictions, but C includes–and C++ retains–the ability to decompose a pointer into a sequence of bytes, assemble any sequence of bytes containing the same values into a pointer, and then use that pointer to access the earlier object.
While that ability can be useful–or even indispensable–in some kinds of applications, a language that includes that ability will be very limited in its ability to support any kind of useful and reliable garbage collection. If a compiler doesn't know everything that has been done with the bits that made up a pointer, it will have no way of knowing whether information sufficient to reconstruct the pointer might exist somewhere in the universe. Since it would be possible for that information to be stored in ways that the computer wouldn't be able to access even if it knew about them (eg the bytes making up the pointer might have been shown on the screen long enough for someone to write them down on a piece of paper), it may be literally impossible for a computer to know whether a pointer could possibly be used in the future.
An interesting quirk of many garbage-collected frameworks is that an object reference not defined by the bit patterns contained therein, but by the relationship between the bits held in the object reference and other information held elsewhere. In C and C++, if the bit pattern stored in a pointer identifies an object, that bit pattern will identify that object until the object is explicitly destroyed. In a typical GC system, an object may be represented by a bit pattern 0x1234ABCD at one moment in time, but the next GC cycle might replace all references to 0x1234ABCD with references to 0x4321BABE, whereupon the object would be represented by the latter pattern. Even if one were to display the bit pattern associated with an object reference and then later read it back from the keyboard, there would be no expectation that the same bit pattern would be usable to identify the same object (or any object).
Mainly for two reasons:
- Because it doesn't need one (IMHO)
- Because it's pretty much incompatible with RAII, which is the cornerstone of C++
C++ already offers manual memory management, stack allocation, RAII, containers, automatic pointers, smart pointers… That should be enough. Garbage collectors are for lazy programmers who don't want to spend 5 minutes thinking about who should own which objects or when should resources be freed. That's not how we do things in C++.