为什么在C#中没有引用计数+垃圾收集?

Skr*_*sli 55 c# garbage-collection reference-counting

我来自C++背景,我和C#一起工作了大约一年.像许多其他人一样,我对于为什么确定性资源管理不是内置于语言中而感到困惑.我们没有确定性的析构函数,而是具有处置模式.人们开始怀疑是否通过他们的代码传播IDisposable癌症是值得的.

在我的C++偏见的大脑中,似乎使用带有确定性析构函数的引用计数智能指针是垃圾收集器的一个重要步骤,它需要您实现IDisposable并调用dispose来清理非内存资源.不可否认,我不是很聪明......所以我纯粹是想要更好地理解为什么事情就是这样.

如果修改了C#,那么:

对象是引用计数.当对象的引用计数变为零时,将在对象上确定性地调用资源清理方法,然后将该对象标记为垃圾回收.垃圾收集在将来某个非确定性时间发生,此时回收内存.在这种情况下,您不必实现IDisposable或记得调用Dispose.如果要释放非内存资源,则只需实现资源清理功能.

  • 为什么这是一个坏主意?
  • 那会破坏垃圾收集器的目的吗?
  • 实施这样的事情是否可行?

编辑:从目前为止的评论,这是一个坏主意,因为

  1. 没有引用计数,GC更快
  2. 处理对象图中的循环的问题

我认为第一名是有效的,但使用弱引用很容易处理第二名.

那么速度优化是否超过你的缺点:

  1. 可能无法及时释放非内存资源
  2. 可能过早释放非内存资源

如果您的资源清理机制是确定性的并且内置于该语言中,则可以消除这些可能性.

Luc*_*cas 49

Brad Abrams 在开发.Net框架期间发布了一封来自Brian Harry的电子邮件.它详细说明了未使用引用计数的许多原因,即使其中一个早期优先事项是使用引用计数的VB6保持语义等效.它研究了一些可能性,例如将某些类型引用计数而不是其他类型(IRefCounted!),或者具有重新计算的特定实例,以及为什么这些解决方案都不被认为是可接受的.

因为[资源管理和确定性最终确定的问题]是一个如此敏感的话题,我将尽力在我的解释中尽可能精确和完整.我为邮件的长度道歉.这封邮件的前90%试图说服你,问题确实很难.在最后一部分中,我将讨论我们正在尝试做的事情,但是您需要了解我们为什么要考虑这些选项的第一部分.

...

我们最初假设解决方案将采用自动引用计数的形式(因此程序员不会忘记)以及其他一些自动检测和处理周期的东西.......我们最终得出的结论是,这在一般情况下不起作用.

...

综上所述:

  • 我们认为解决循环问题非常重要, 而不必强迫程序员理解,跟踪和设计这些复杂的数据结构问题.
  • 我们希望确保我们拥有高性能(速度和工作集)系统,我们的分析表明,对系统中的每个对象使用引用计数将无法实现此目标.
  • 由于各种原因,包括组合和铸造问题,没有简单的透明解决方案,只需要对那些需要它的对象进行重新计算.
  • 我们选择不选择为单个语言/上下文提供确定性最终化的解决方案,因为它禁止与其他语言互操作并通过创建特定于语言的版本导致类库的分叉.

  • 绝对阅读完整的邮件 - 它非常详细地解释了决定背后的原因. (7认同)
  • 微软已经破坏了他们所有的博客链接。我认为这是本回复中链接的[资源管理](https://docs.microsoft.com/en-gb/archive/blogs/brada/resource-management)文章的新家。我花了一段时间才找到。 (3认同)

Gis*_*shu 31

垃圾收集器不要求您为您定义的每个类/类型编写Dispose方法.您只需在需要明确执行清理操作时定义一个; 当您明确分配本机资源时.大多数情况下,即使您只对对象执行new()操作,GC也会回收内存.

GC确实引用了计数 - 但是它通过每次进行集合时找到哪些对象是"可到达的"(Ref Count > 0)来以不同的方式进行计数......它只是不以整数计数器的方式进行计数..收集无法访问的对象().这样,每次分配或释放对象时,运行时不必执行内务处理/更新表...应该更快.Ref Count = 0

C++(确定性)和C#(非确定性)之间唯一的主要区别是清理对象时.您无法预测在C#中收集对象的确切时刻.

无数插件:如果您真的对GC的工作方式感兴趣,我建议您通过C#阅读Jeffrey Richter关于CLR中 GC的立场章节.

  • 实际上,你没有__ve_在IDisposable的每个实现者上调用dispose.如果对象编写良好/具有终结器,则仍将收集资源.IDisposable提供了一种确定性释放的方法,如果你将它确定为确定性的.如果您不经常获取特定的OS资源,那么非确定性的终结可能就足够了.可以把它想象为将经典的C++"选择加入"概念扩展到资源管理. (8认同)
  • 因此,避免使用RAII,因为引用计数较慢,并且您不必在每个类上处理Dispose.对我来说似乎是一个糟糕的权衡. (3认同)
  • 不明白你在这里的评论...我只是解释说实现 Dispose 并不是每个 C# 类型/类都强制执行的。即使您的类没有实现 Dispose,GC 也能正常工作。 (2认同)
  • @Skrymsli:Rex M是对的.并且可以安全地假设框架中的所有内容(这是您在我之前的评论中提到的内容)都写得很好. (2认同)

use*_*032 23

在C#中尝试了引用计数.我相信,那些发布了Rotor(CLR的参考实现,其中有源可用)的人确实参考了基于计数的GC,只是为了看看它与世代相比如何.结果令人惊讶 - "股票"GC的速度要快得多,甚至都不好笑.我不记得我听到的确切位置,我认为这是Hanselmuntes的一个播客.如果你想看到C++在与C#的性能比较中得到了基本压力 - 谷歌Raymond Chen的中文词典应用程序.他做了一个C++版本,然后Rico Mariani做了一个C#.我认为Raymond 6迭代最终击败了C#版本,但到那时他不得不放弃C++的所有漂亮的面向对象,并进入win32 API级别.整个事情变成了性能黑客.同时,C#程序只进行了一次优化,最后仍然看起来像是一个体面的OO项目

  • @Simon:尽管如此,事实仍然是,使用当前可用的工具,编写c#代码比编写表现良好的c ++代码表现得更好,更快,更容易出错. (8认同)
  • @Unknown:"[引用计数]具有较低的最大暂停时间,并且通常会尽快释放内存".相反,引用计数引入了无限制的暂停时间,因为整个堆可以在雪崩的析构函数中收集,并且引用计数推迟收集直到范围结束,这可以(并且通常是)比活动结束更晚. (7认同)
  • 从内存中我不认为Rotor运行时非常优化,所以我认为它不是一个非常好的数据点.+1字典应用程序引用很有趣.以下是供参考的链接:http://blogs.msdn.com/ricom/archive/2005/05/10/416151.aspx (6认同)
  • 已知参考计数总体上比追踪GC慢.但是,它具有较低的最大暂停时间,并且通常会尽快释放内存. (3认同)

Luk*_*ane 14

C++样式智能指针引用计数和引用计数垃圾回收之间存在差异.我也谈到了我博客上的差异,但这里有一个简短的总结:

C++样式引用计数:

  • 递减的无限成本:如果大数据结构的根减少到零,则释放所有数据的成本无限制.

  • 手动循环收集:为防止循环数据结构泄漏内存,程序员必须通过用弱智能指针替换部分循环来手动破坏任何潜在的结构.这是潜在缺陷的另一个来源.

参考计数垃圾收集

  • 延迟RC:对堆栈和寄存器引用忽略对象引用计数的更改.相反,当触发GC时,通过收集根集来保留这些对象.可以推迟并批量处理对引用计数的更改.这导致更高的吞吐量.

  • 合并:使用写屏障可以合并对引用计数的更改.这使得可以忽略对象引用计数的大多数更改,从而提高频繁变异引用的RC性能.

  • 循环检测:对于完整的GC实施,还必须使用循环检测器.然而,可以以递增方式执行循环检测,这反过来意味着有限的GC时间.

基本上,可以为运行时实现基于高性能RC的垃圾收集器,例如Java的JVM和.net CLR运行时.

我认为跟踪收集器部分是由于历史原因而使用的:在JVM和.net运行时发布后,引用计数的许多最近的改进都来了.研究工作也需要时间来过渡到生产项目.

确定性资源处置

这几乎是一个单独的问题..net运行时使用IDisposable接口使这成为可能,例如下面的例子.我也喜欢Gishu的回答.


@Skrymsli,这是" 使用 "关键字的目的.例如:

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) { }
}
Run Code Online (Sandbox Code Playgroud)

然后添加一个包含关键资源的类:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后使用它就像:

using (ComFileCritical fileResource = new ComFileCritical()) {
    // Some actions on fileResource
}

// fileResource's critical resources freed at this point
Run Code Online (Sandbox Code Playgroud)

另请参阅正确实现IDisposable.

  • 有趣的博客帖子......我不太关心释放内存而不是释放可能对时间至关重要的非内存资源.(想想一个包含文件锁的COM对象的句柄.你希望在完成它时释放锁,而不是在GC将来运行终结器时.)我认为必须有一些组合GC和智能指针提供两全其美的优势,您可以获得卓越的内存管理,但可以确定地释放关键资源,而不会给程序员带来不必要的负担. (2认同)
  • "可以为运行时实现基于高性能RC的垃圾收集器,例如Java的JVM和.net CLR运行时".生产GC使用跟踪代替RC,因为事实并非如此. (2认同)

Jon*_*rop 6

我来自C++背景,我和C#一起工作了大约一年.像许多其他人一样,我对于为什么确定性资源管理不是内置于语言中而感到困惑.

using构造提供了"确定性"资源管理,并构建在C#语言中.请注意,通过"确定性",我的意思Dispose是保证在using块开始执行之后在代码之前调用.还要注意,这不是"确定性"这个词的意思,但每个人似乎都在这种情况下滥用它,这很糟糕.

在我的C++偏见的大脑中,似乎使用带有确定性析构函数的引用计数智能指针是垃圾收集器的一个重要步骤,它需要您实现IDisposable并调用dispose来清理非内存资源.

垃圾收集器不需要您实现IDisposable.实际上,GC完全没有注意到它.

不可否认,我不是很聪明......所以我纯粹是想要更好地理解为什么事情就是这样.

跟踪垃圾收集是一种快速可靠的模拟无限内存机器的方法,使程序员免于手动内存管理的负担.这消除了几类错误(悬空指针,免费太快,双重免费,忘记免费).

如果修改了C#,那么:

对象是引用计数.当对象的引用计数变为零时,将在对象上确定性地调用资源清理方法,

考虑两个线程之间共享的对象.线程竞相将引用计数减少到零.一个线程将赢得比赛,另一个将负责清理.这是不确定的.引用计数本质上是确定性的信念是一个神话.

另一个常见的误解是引用计数可以在程序中尽可能早地释放对象.它没有.递减总是推迟,通常到范围的末尾.这使对象保持活动的时间超过了必要的时间,留下了所谓的"浮动垃圾".请注意,特别是,某些跟踪垃圾收集器可以比基于范围的引用计数实现更早地回收对象.

然后该对象被标记为垃圾收集.垃圾收集在将来某个非确定性时间发生,此时回收内存.在这种情况下,您不必实现IDisposable或记得调用Dispose.

IDisposable无论如何,您不必为垃圾收集对象实现,因此这是无益的.

如果要释放非内存资源,则只需实现资源清理功能.

为什么这是一个坏主意?

天真的引用计数非常慢并且会发生泄漏.例如,C++中的Boost shared_ptr比OCaml的跟踪GC慢10倍.即使是天真的基于范围的引用计数在存在多线程程序(几乎所有现代程序)的情况下都是非确定性的.

那会破坏垃圾收集器的目的吗?

一点也不,不.事实上,这是一个糟糕的想法,是在20世纪60年代发明的,并在接下来的54年中接受了强烈的学术研究,得出的结论是在一般情况下引用计数很糟糕.

实施这样的事情是否可行?

绝对.早期原型.NET和JVM使用引用计数.他们还发现它吸引并放弃它,有利于追踪GC.

编辑:从目前为止的评论,这是一个坏主意,因为

没有引用计数,GC更快

是.请注意,您可以通过延迟计数器增量和减量来更快地进行引用计数,但这会牺牲您非常渴望的确定性,并且仍然比使用今天的堆大小跟踪GC更慢.然而,引用计数渐近地更快,因此在未来的某些时候,当堆积变得非常大时,我们将开始在生产自动化内存管理解决方案中使用RC.

处理对象图中的循环的问题

试验删除是一种专门用于检测和收集参考计数系统中的循环的算法.然而,它是缓慢且不确定的.

我认为第一名是有效的,但使用弱引用很容易处理第二名.

将弱引用称为"简单"是希望胜过现实的胜利.他们是一场噩梦.它们不仅难以预测且难以构建,而且还污染了API.

那么速度优化是否超过你的缺点:

可能无法及时释放非内存资源

是不是及时using释放非内存资源?

可能过早释放非内存资源如果您的资源清理机制是确定性的并且内置于该语言,则可以消除这些可能性.

using构造是确定性的并且构建在语言中.

我想你真正想问的问题是为什么不IDisposable使用引用计数.我的回答是轶事:我已经使用垃圾收集语言18年了,我从来不需要求助于引用计数.因此,我更喜欢更简单的API,这些API没有像弱引用这样的偶然复杂性而受到污染.


Unk*_*own 5

我对垃圾收集知之甚少.这是一个简短的总结,因为完整的解释超出了这个问题的范围.

.NET使用复制和压缩的分代垃圾收集器.这比引用计数更先进,并且具有能够直接或通过链收集引用自身的对象的好处.

引用计数不会收集周期.引用计数也具有较低的吞吐量(总体较慢),但具有比跟踪收集器更快的暂停(最大暂停较小)的益处.