Why no Reference Counting + Garbage Collection in C#?
我来自C++背景,我和C一直工作了大约一年。像其他许多人一样,我对为什么确定性资源管理不内置于语言感到困惑。我们有Dispose模式,而不是确定性析构函数。人们开始怀疑通过他们的代码传播易感染的癌症是否值得这么做。
在我的C++偏颇的大脑中,好像使用引用计数的智能指针,确定性析构函数是垃圾回收器的一个主要步骤,它要求您实现IDISPOSIDLE和调用DATABLE来清理非内存资源。不可否认,我不是很聪明…所以我问这个纯粹是为了更好地理解为什么事情是这样的。
如果C被修改为:
对象被引用计数。当一个对象的引用计数为零时,对该对象调用一个资源清理方法,然后将该对象标记为垃圾收集。垃圾收集发生在将来某个不确定的时间,此时内存被回收。在这种情况下,您不必实现IDisposable或记住调用Dispose。如果要释放非内存资源,则只需实现资源清理功能。
- 为什么这是个坏主意?
- 这会破坏垃圾回收的目的吗?
- 实施这样一件事是可行的吗?
编辑:从目前的评论来看,这是个坏主意,因为
我认为数字1是有效的,但数字2使用弱引用很容易处理。
因此,速度优化是否会超过以下缺点:
如果您的资源清理机制是确定性的,并且内置于语言中,那么您可以消除这些可能性。
BradAbrams发布了一封来自BrianHarry的电子邮件,这封邮件是在开发.NET框架时写的。它详细说明了没有使用引用计数的许多原因,即使早期的优先事项之一是保持与使用引用计数的vb6的语义等价。它研究了一些可能性,例如计算了一些类型的引用,而没有计算其他类型的引用(
Because [the issue of resource
management and deterministic
finalization] is such a
sensitive topic I am going to try to
be as precise and complete in my
explanation as I can. I apologize for
the length of the mail. The first 90%
of this mail is trying to convince you
that the problem really is hard. In
that last part, I'll talk about things
we are trying to do but you need the
first part to understand why we are
looking at these options....
We initially started with the
assumption that the solution would
take the form of automatic ref
counting (so the programmer couldn't
forget) plus some other stuff to
detect and handle cycles
automatically. ...we ultimately concluded that
this was not going to work in the
general case....
In summary:
- We feel that it is very important to solve the cycle problem
without forcing programmers to
understand, track down and design
around these complex data structure
problems.- We want to make sure we have a high performance (both speed and
working set) system and our analysis
shows that using reference counting
for every single object in the system
will not allow us to achieve this
goal.- For a variety of reasons, including composition and casting
issues, there is no simple transparent
solution to having just those objects
that need it be ref counted.- We chose not to select a solution that provides deterministic
finalization for a single
language/context because it inhibits
interop with other languages and
causes bifurcation of class libraries
by creating language specific
versions.
垃圾收集器不要求您为定义的每个类/类型编写Dispose方法。当您需要显式地做一些清理工作时,您只定义一个;当您显式地分配了本机资源时。大多数情况下,GC只是回收内存,即使您只对对象执行new()之类的操作。
gc进行引用计数-但是它通过查找每次执行收集时"可访问"的对象(
C++(确定性)和C(非确定性)之间唯一的主要区别是当对象被清理时。你无法预测一个对象在C中的确切收集时间。
无数次插拔:我建议你读一下杰弗里·里克特通过C写的关于CLR中GC的站立章节,以防你真的对GC的工作方式感兴趣。
在C中尝试了参考计数。我相信,那些发布rotor(一个提供源代码的clr的参考实现)的人确实引用了基于计数的gc,只是为了看看它将如何与世代的gc进行比较。结果令人惊讶,"stock"GC速度更快,甚至不好笑。我不清楚我在哪里听到的,我想那是汉斯孟特的播客之一。如果你想看到C++GET基本上被压碎的性能比较,与谷歌Raymond Chen的中文词典应用程序比较。他做了一个C++版本,然后Rico Mariani做了一个C版本。我认为雷蒙德需要6次迭代来最终打败C版本,但到那时,他必须放弃所有优秀的C++对象,并进入Win32 API级别。整个事情都变成了一个性能黑客。同时,C程序只优化了一次,最终看起来还是一个不错的OO项目。
C++风格的智能指针引用计数和引用计数垃圾收集有区别。我也在我的博客上讨论过这些差异,但这里有一个简短的总结:< BR>< BR>
C++样式引用计数:递减时的无边界成本:如果一个大型数据结构的根被递减到零,则有一个无边界成本来释放所有数据。
手动循环收集:为了防止循环数据结构泄漏内存,程序员必须用一个弱的智能指针替换循环的一部分,从而手动破坏任何潜在的结构。这是潜在缺陷的另一个来源。
引用计数垃圾收集
延迟rc:堆栈和寄存器引用忽略对对象引用计数的更改。相反,当触发GC时,这些对象通过收集根集来保留。对引用计数的更改可以延迟并分批处理。这将导致更高的吞吐量。
合并:使用写屏障可以合并对引用计数的更改。这使得可以忽略对对象引用计数的大多数更改,从而提高频繁变化的引用的rc性能。
循环检测:对于完整的GC实现,还必须使用循环检测器。然而,可以以增量的方式执行循环检测,这反过来意味着有限的GC时间。
基本上,可以实现一个高性能的基于RC的垃圾收集器,用于运行时,如Java的JVM和.NET CLR运行时。
我认为跟踪收集器的使用部分是出于历史原因:最近在引用计数方面的许多改进都是在JVM和.NET运行时发布之后才开始的。研究工作也需要时间过渡到生产项目。< BR>< BR>
确定性资源处置这几乎是一个单独的问题。.NET运行时使使用IDisposable接口成为可能,如下示例。我也喜欢Gishu的回答。
@skrymsli,这是"使用"关键字的目的。例如。:
1 2 3 4 5 6 7 8 9 10 11 12 | public abstract class BaseCriticalResource : IDiposable { ~ BaseCriticalResource () { Dispose(false); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); // No need to call finalizer now } protected virtual void Dispose(bool disposing) { } } |
然后添加具有关键资源的类:
1 2 3 4 5 6 7 8 9 10 11 12 | public class ComFileCritical : BaseCriticalResource { private IntPtr nativeResource; protected override Dispose(bool disposing) { // free native resources if there are any. if (nativeResource != IntPtr.Zero) { ComCallToFreeUnmangedPointer(nativeResource); nativeResource = IntPtr.Zero; } } } |
然后使用它就简单到:
1 2 3 4 5 | using (ComFileCritical fileResource = new ComFileCritical()) { // Some actions on fileResource } // fileResource's critical resources freed at this point |
另请参见正确实现IDisposable。
I come from a C++ background and I've been working with C# for about a year. Like many others I'm flummoxed as to why deterministic resource management is not built-in to the language.
In my C++-biased brain it seems like using reference-counted smart pointers with deterministic destructors is a major step up from a garbage collector that requires you to implement IDisposable and call dispose to clean up your non-memory resources.
垃圾收集器不要求您实现
Admittedly, I'm not very smart... so I'm asking this purely from a desire to better understand why things are the way they are.
跟踪垃圾收集是模拟无限内存机的一种快速、可靠的方法,从而使程序员摆脱了手动内存管理的负担。这消除了几类错误(悬空指针、释放得太快、双重释放、忘记释放)。
What if C# were modified such that:
Objects are reference counted. When an object's reference count goes to zero, a resource cleanup method is called deterministically on the object,
考虑两个线程之间共享的对象。线程竞相将引用计数递减为零。一个线程将赢得比赛,另一个线程将负责清理。这是不确定性的。认为参考计数本质上是确定性的观点是一个神话。
另一个常见的神话是引用计数在程序中最早可能的点释放对象。它没有。减量总是被延迟,通常是在范围的末尾。这样可以使对象的存活时间比需要的时间长,从而使所谓的"漂浮垃圾"随处可见。请注意,特别是,一些跟踪垃圾收集器可以并且可以在基于范围的引用计数实现之前回收对象。
then the object is marked for garbage collection. Garbage collection occurs at some non-deterministic time in the future at which point memory is reclaimed. In this scenario you don't have to implement IDisposable or remember to call Dispose.
不管怎样,您不必为垃圾收集对象实现
You just implement the resource cleanup function if you have non-memory resources to release.
Why is that a bad idea?
幼稚的引用计数非常缓慢,并且泄漏循环。例如,在C++中,Boost的EDOCX1 5度比OCAM的跟踪GC慢10倍。即使是基于范围的引用计数,在多线程程序(几乎是所有现代程序)的存在下也是不确定的。
Would that defeat the purpose of the garbage collector?
一点也不,不。事实上,这是一个坏主意,它是在20世纪60年代发明的,在接下来的54年里受到了激烈的学术研究,结论是参考计数在一般情况下很糟糕。
Would it be feasible to implement such a thing?
当然。早期的原型.NET和JVM使用了引用计数。他们还发现它被吸走了,并将其丢弃,以便追踪GC。
EDIT: From the comments so far, this is a bad idea because
GC is faster without reference counting
对。注意,通过延迟计数器的递增和递减,您可以使引用计数更快,但是这样会牺牲您非常渴望的确定论,而且它仍然比使用当前堆大小跟踪GC慢。但是,引用计数渐进地更快,所以在将来某个时候,当堆变得非常大时,我们可能会开始在生产自动化内存管理解决方案中使用rc。
problem of dealing with cycles in the object graph
试删除是一种在参考计数系统中专门设计用来检测和收集循环的算法。然而,它是缓慢和不确定性的。
I think number one is valid, but number two is easy to deal with using weak references.
将弱引用称为"容易"是希望战胜现实的胜利。他们是一场噩梦。它们不仅不可预测而且难以构建,而且还会污染API。
So does the speed optimization outweigh the cons that you:
may not free a non-memory resource in a timely manner
might free a non-memory resource too soon
If your resource cleanup mechanism is deterministic and built-in to the language you can eliminate those possibilities.
我认为你真正想问的问题是,为什么江户十一〔三〕不使用参考计数法。我的回答是一个轶事:我已经使用垃圾收集语言18年了,我从来没有必要求助于引用计数。因此,我更喜欢简单的API,它们不会受到弱引用等附带复杂性的污染。
我对垃圾收集有些了解。这是一个简短的总结,因为完整的解释超出了这个问题的范围。
.NET使用复制和压缩一代垃圾收集器。这比引用计数更先进,并且有利于收集直接或通过链引用自身的对象。
参考计数不会收集周期。引用计数也具有较低的吞吐量(总体速度较慢),但与跟踪收集器相比,具有更快的暂停(最大暂停较小)的好处。
这里有很多问题。首先,您需要区分释放托管内存和清理其他资源。前者可能非常快,而后者可能非常慢。在.NET中,两者是分开的,这样可以更快地清理托管内存。这还意味着,只有当您有超出托管内存范围的内容需要清理时,才应该实现Dispose/Finalizer。
.NET使用了标记和扫描技术,通过堆查找对象的根。根实例在垃圾收集中存活。其他的一切都可以通过回收内存来清理。GC必须时不时地压缩内存,但除此之外,即使在回收多个实例时,回收内存也是一个简单的指针操作。将此与C++中的析构函数的多个调用进行比较。
确定性非内存资源管理是语言的一部分,但是它不是用析构函数完成的。
您的意见在来自C++背景的人中很常见,试图使用RAII设计模式。在C++中,您可以保证某些代码将在一个作用域的末端运行,即使抛出一个请求,也就是在堆栈上分配一个对象,并将清理代码放入析构函数中。
在其他语言(C语言,Java,Python,Ruby,Erlang,…)中,您可以使用TestEnter(或者最后尝试catch),以确保清理代码始终运行。
1 2 3 4 5 6 7 8 | // Initialize some resource. try { // Use the resource. } finally { // Clean-up. // This code will always run, whether there was an exception or not. } |
i c_,您也可以使用using结构:
1 2 3 4 5 | using (Foo foo = new Foo()) { // Do something with foo. } // foo.Dispose() will be called afterwards, even if there // was an exception. |
因此,对于C++程序员来说,考虑"运行清理代码"和"释放内存"可能是两个独立的事情。将清理代码放在finally块中,然后留给GC来处理内存。
参考计数
使用引用计数的成本是双重的:首先,每个对象都需要特殊的引用计数字段。通常,这意味着必须在每个对象中分配一个额外的存储字。第二,每次将一个参考分配给另一个参考时,必须调整参考计数。这显著增加了赋值语句所花费的时间。
.NET中的垃圾收集
C不使用对象的引用计数。相反,它维护一个来自堆栈的对象引用图,并从根目录导航以覆盖所有被引用的对象。图中的所有被引用对象都在堆中压缩,以便将来的对象可以使用连续的内存。回收所有不需要最终确定的未引用对象的内存。那些未被引用但要在其上执行终结器的对象将被移动到一个称为F-reachable队列的单独队列中,在该队列中,垃圾收集器在后台调用其终结器。
除了上述的GC之外,还使用了生成的概念来实现更高效的垃圾收集。它基于以下概念1。压缩托管堆的一部分内存比压缩整个托管堆更快。2。较新的对象寿命较短,较旧的对象寿命较长三。较新的对象往往相互关联,并由应用程序同时访问。
托管堆分为三代:0、1和2。新对象存储在gen 0中。GC循环未回收的对象将升级到下一代。因此,如果第0代中的较新对象在GC循环1之后仍存在,则它们将升级到第1代。其中那些在GC循环2中存活下来的被提升到第2代。因为垃圾收集器只支持三代,所以第2代中在收集后仍存在的对象将保留在第2代中,直到确定在将来的收集中无法访问它们为止。
垃圾收集器在第0代已满且需要为新对象分配内存时执行收集。如果第0代的集合没有回收足够的内存,垃圾收集器可以执行第1代的集合,然后执行第0代。如果这不能回收足够的内存,垃圾收集器可以执行第2、1和0代的收集。
因此,GC比引用计数更有效。
当用户未显式调用Dispose时,实现IDisposable的对象还必须实现由GC调用的终结器-请参阅位于msdn的IDisposable.Dispose。
IDisposable的关键是GC在某个不确定的时间运行,而您实现IDisposable是因为您拥有一个有价值的资源,并且希望在确定的时间释放它。
所以你的提议不会改变我的身份。
编辑:
对不起的。没有正确阅读你的建议。:
维基百科对引用计数GC的缺点有一个简单的解释。