首页 文章

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

提问于
浏览
53

我来自C背景,并且为了确保资源管理没有内置到语言中而我感到很沮丧 . 我们没有确定性的析构函数,而是具有处置模式 . People start to wonder是否通过他们的代码传播IDisposable癌症值得努力 .

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

如果修改了C#,那么:

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

  • 为什么这是一个坏主意?

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

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

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

  • GC在没有引用计数的情况下更快

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

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

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

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

  • 可能会过早释放非内存资源

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

10 回答

  • 1

    这里有很多问题在起作用 . 首先,您需要区分释放托管内存和清理其他资源 . 前者可能非常快,而后者可能非常慢 . 在.NET中,两者是分开的,这样可以更快地清理托管内存 . 这也意味着,当你有超出托管内存的东西要清理时,你应该只实现Dispose / Finalizer .

    .NET采用标记和扫描技术,遍历堆查找对象的根 . Rooted实例在垃圾收集中存活 . 只需回收内存即可清除其他所有内容 . GC必须时不时地压缩内存,但除了回收内存之外,即使在回收多个实例时也是一个简单的指针操作 . 将此与C中对析构函数的多次调用进行比较 .

  • 14

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

    因为[资源管理和确定性最终确定的问题]是一个如此敏感的话题,我将尽力在我的解释中尽可能精确和完整 . 我为邮件的长度道歉 . 这封邮件的前90%试图说服你,问题确实很难 . 在最后一部分中,我将讨论我们正在尝试做的事情,但是您需要了解我们为什么要考虑这些选项的第一部分 . ...我们最初假设解决方案采用自动重新计数的形式(因此程序员不会忘记)加上一些其他东西来自动检测和处理循环 . ......我们最终得出的结论是,这在一般情况下不起作用 . ...总结:我们认为解决循环问题非常重要,而不必强迫程序员理解,跟踪和设计这些复杂的数据结构问题 . 我们希望确保我们拥有高性能(速度和工作集)系统,我们的分析表明,对系统中的每个对象使用引用计数将无法实现此目标 . 由于各种原因,包括组合和铸造问题,没有简单的透明解决方案,只需要对那些需要它的对象进行重新计算 . 我们选择不选择为单个语言/上下文提供确定性最终化的解决方案,因为它禁止与其他语言互操作并通过创建特定于语言的版本导致类库的分叉 .

  • 6

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

    GC确实引用了计数 - 但是它通过查找哪些对象'reachable'( Ref Count > 0every time it does a collection 以不同的方式执行...它只是不以整数形式执行反方式 . . 收集无法访问的对象( Ref Count = 0 ) . 这样,每次分配或释放对象时,运行时不必执行内务处理/更新表...应该更快 .

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

    无数次插件:我在CLR via C#中关于GC的立场章节,以防您对GC的工作原理感兴趣 .

  • 30

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

  • 1

    C风格的智能指针引用计数和引用计数垃圾回收之间存在差异 . 我也谈到了my blog的差异,但这里有一个简短的总结:

    C样式参考计数:

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

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

    参考计数垃圾收集

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

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

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

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

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

    确定性资源处置

    这几乎是一个单独的问题 . .net运行时使用IDisposable接口实现了这一点,例如下面的例子 . 我也喜欢Gishu's回答 .


    @Skrymsli,这是“using”关键字的目的 . 例如 . :

    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) { }
    }
    

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

    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;
            }
        }
    }
    

    然后使用它就像:

    using (ComFileCritical fileResource = new ComFileCritical()) {
        // Some actions on fileResource
    }
    
    // fileResource's critical resources freed at this point
    

    另见implementing IDisposable correctly .

  • 48

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

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

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

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

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

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

    如果修改了C#,那么:对象被引用计数 . 当对象的引用计数变为零时,将在对象上确定性地调用资源清理方法,

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

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

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

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

    如果要释放非内存资源,则只需实现资源清理功能 . 为什么这是一个坏主意?

    天真的引用计数非常慢并且会发生泄漏 . 例如,Boost's shared_ptr in C++ is up to 10x slower than OCaml's tracing GC . 即使是天真的基于范围的引用计数在存在多线程程序(几乎所有现代程序)的情况下都是非确定性的 .

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

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

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

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

    编辑:从目前为止的评论来看,这是一个坏主意,因为没有引用计数,GC更快

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

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

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

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

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

    速度优化也超过了你的缺点:可能无法及时释放非内存资源

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

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

    using 构造是确定性的并且内置于该语言中 .

    我想你真正想问的问题是为什么 IDisposable 不使用引用计数 . 我的回答是轶事:我被污染了像弱引用这样的偶然复杂性 .

  • 4

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

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

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

  • 5

    当用户没有显式调用Dispose时,实现IDisposable的对象还必须实现GC调用的终结器 - 请参阅IDisposable.Dispose at MSDN .

    IDisposable的重点在于GC在某个非确定性的时间运行,并且您实现了IDisposable,因为您拥有一个有 Value 的资源,并希望在确定的时间释放它 .

    所以你的提议在IDisposable方面没有任何改变 .

    编辑:

    抱歉 . 没有正确阅读您的提案 . :-(

    维基百科有一个简单的解释shortcomings of References counted GC

  • 20

    Reference count

    使用引用计数的成本是双重的:首先,每个对象都需要特殊的引用计数字段 . 通常,这意味着必须在每个对象中分配额外的存储字 . 其次,每次将一个参考分配给另一个参考时,必须调整参考计数 . 这大大增加了赋值语句所花费的时间 .

    Garbage Collection in .NET

    C#不使用对象的引用计数 . 相反,它维护堆栈中对象引用的图形,并从根目录导航以覆盖所有引用的对象 . 图中的所有引用对象都在堆中压缩,以便连续的内存可用于将来的对象 . 回收所有不需要最终确定的未引用对象的内存 . 那些未被引用但具有要在其上执行的终结器的那些被移动到称为f-reachable队列的单独队列中,其中垃圾收集器在后台调用它们的终结器 .

    除了上面的GC之外,还使用了几代的概念来实现更高效的垃圾收集 . 它基于以下概念1.为托管堆的一部分压缩内存比整个托管堆更快2.新对象的生命周期更短,旧对象的生命周期更长3.新对象倾向于相互关联,并由应用程序在同一时间访问

    托管堆分为三代:0,1和2.新对象存储在gen 0中 . 不被GC循环回收的对象被提升到下一代 . 因此,如果gen 0中的较新对象在GC循环1中存活,那么它们将被提升为gen 1.在GC循环2中存活的那些将被提升为gen 2.因为垃圾收集器仅支持三代,所以第2代中的对象继续存在于第2代,直到他们确定在未来的集合中无法访问 .

    当生成0已满并且需要分配新对象的内存时,垃圾收集器执行集合 . 如果第0代的集合没有回收足够的内存,则垃圾收集器可以执行第1代的集合,然后生成0.如果这不能回收足够的内存,则垃圾收集器可以执行第2代,第1代和第0代的集合 .

    因此GC比参考计数更有效 .

  • 1

    确定性非内存资源管理是语言的一部分,但是它不是用析构函数完成的 .

    您的观点在来自C背景的人中很常见,试图使用RAII设计模式 . 在C中,保证某些代码将在作用域的末尾运行的唯一方法是,即使抛出一个异常,也就是在堆栈上分配一个对象并将清理代码放在析构函数中 .

    在其他语言(C#,Java,Python,Ruby,Erlang,...)您可以使用try-finally(或try-catch-finally)来确保清理代码始终运行 .

    // Initialize some resource.
    try {
        // Use the resource.
    }
    finally {
        // Clean-up.
        // This code will always run, whether there was an exception or not.
    }
    

    我是C#,你也可以使用using构造:

    using (Foo foo = new Foo()) {
        // Do something with foo.
    }
    // foo.Dispose() will be called afterwards, even if there
    // was an exception.
    

    因此,对于C程序员来说,将“运行清理代码”和“释放内存”视为两个独立的事情可能会有所帮助 . 将清理代码放在finally块中,然后留给GC来处理内存 .

相关问题