拥有 Finalizer 的开销 - with/without 在 Dispose 中 SuppressFinalize

Overhead of having a Finalizer - with/without SuppressFinalize in Dispose

假设如下:

class 有 3 种可能的 IDisposable 实现:

  1. IDisposable 成员上调用 Dispose() 的最小 Dispose 方法 - 无终结器
  2. 带有终结器的标准 IDisposable 实现 缺少 Dispose().
  3. 中通常的 GC.SuppressFinalize(this) 调用
  4. 带有终结器的完整标准 IDisposable 实现(以及 Dispose() 中的 GC.SuppressFinalize(this) 调用)。

下列说法正确的是?我理解正确吗?

  1. 情况 A. 比 B. 和 C. 的开销略小,因为对象没有终结器,所以它不会进入 GC 终结队列 - 因此 GC 可以在早期清除该对象收集 - 无开销。
  2. 情况 B。该对象有一个终结器,因此将在 GC 终结器队列中结束,并且终结器将被调用(因为它没有被抑制)- 终结器调用 dispose 什么都不做,因为它已经被调用了。这会导致对象在终结器队列中的开销很小,并且终结器调用的开销也很小。
  3. 情况 C。该对象有一个终结器,因此仍将在 GC 终结器队列中结束。因为处置并因此 SuppressFinalize 已被调用,终结器不会 运行。这种情况仍然会导致对象进入终结器队列的开销很小,但终结器实际上并不 运行.

这里的关键点是 "I've avoided the finalizer overhead by calling SuppressFinalize" 很诱人 - 但我认为( 并想澄清 )这是不正确的。对象在终结器队列中的开销仍然会产生——你要避免的只是实际的终结器调用——在常见情况下,这只是 "I'm disposed already do nothing".

注意:这里的 "Full standard IDisposable implementation" 我指的是旨在涵盖非托管和托管资源情况的标准实现(注意这里我们只有托管对象成员)。

public void Dispose() {
    Dispose(true);
    GC.SuppressFinalize(this);
}

private bool _disposed;
protected virtual void Dispose(bool disposing) {
if (_disposed)
    return;
    if (disposing) {
        // dispose managed members...
    }
    _disposed = true;
}

~AXCProcessingInputs() {
    Dispose(false);
}

您应该只在需要清理非托管资源的对象上包含终结器。由于您只有托管成员,因此不需要终结器 - 如果成员有终结器并且不为他们调用 GC.SuppressFinalize(),则终结器将 运行 作用于成员本身。

终结器的目标是在 GC 需要时清理非托管资源及其托管包装器,而 Dispose 模式的目标是清理任何类型的资源 具体时刻.

没有人应该想到 "Ive avoided the finalizer overhead by calling SuppressFinalize" - 相反他们应该想到 "I've cleaned up my unmanaged resources, there is no need for the finalizer to run"。

关于编号问题:

  1. 是的,有一个终结器 运行 会在收集时产生一些开销
  2. 是,在已处置的对象上调用 Dispose() should be ignored
  3. 这些 类 在创建实例时被添加到终结队列,但在 GC 尝试收集它时不会添加到 freachable 队列 - 将对象排队只是为了忽略是没有意义的稍后。另见 this link.

不同版本的 .NET GC 可能做事不同,但根据我的理解,任何带有 Finalize 方法的 object 都将添加到 "finalizer queue"([= 列表35=]s 已请求通知(如果放弃),并且只要它存在就会保留在 queue 中。注销和重新注册最终确定的方法(恕我直言应该是 Object 的受保护成员)设置或清除 object header 中的标志,该标志控制 object 是否应该移动到 "freachable queue"(object 的列表,其 finalize 方法应尽快 运行 )如果被发现被放弃,但不会导致 object 要从终结器中添加或删除 queue.

因此,覆盖 Finalize 的每种类型的每个实例都会对 它参与的每个 garbage-collection 周期施加一个小但 non-zero 的开销, 只要它存在。在放弃之前在 object 上调用 SuppressFinalize 将阻止它被移动到 freachable queue,但不会消除由于它已经在 finalizable queue 中而产生的开销] 自始至终。

我建议 public-facing object 永远不要实施 FinalizeFinalize 方法有一些合法用途,但是重写它的 类 应该避免持有对终结不需要的任何内容的引用(有点烦人,如果可终结的 object 持有对弱引用,弱引用可能在终结器 运行s 之前失效,即使弱引用的目标仍然存在)。

我有时会使用终结器进行调试,以检查我是否在某处遗漏了一些处置。如果有人感兴趣,我对我的系统进行了快速测试以检查性能影响(Windows 10、.Net 4.7.1、Intel Core i5-8250U)。

添加终结器并抑制它的成本大约为每个对象 60 ns,添加它并忘记调用 dispose 的成本大约为 800每个对象 ns。性能影响与 debug/release 构建和 with/without 附加的调试器非常一致,可能是因为垃圾收集器在两个构建中是相同的。

添加终结器和抑制它对性能的影响是最小的,除非您正在构建大量这些对象,而通常情况并非如此。甚至 Microsoft 自己的 Task 也使用终结器(几乎总是被抑制),而 class 意味着非常轻量级和高性能。所以他们显然同意。

然而,让终结器 运行 变得非常糟糕。考虑到我的测试用例使用了一个没有引用对象的普通 class,它已经慢了一个数量级。拥有大量引用的对象应该花费更多,因为所有这些对象都需要在额外的一代中保持活动状态。这也可能导致在垃圾收集的压缩阶段发生大量复制。

测试源代码:

using System;
using System.Diagnostics;

namespace ConsoleExperiments
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            GenerateGarbageNondisposable();
            GenerateGarbage();
            GenerateGarbageWithFinalizers();
            GenerateGarbageFinalizing();

            var sw = new Stopwatch();

            const int garbageCount = 100_000_000;

            for (var repeats = 0; repeats < 4; ++repeats)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbageNondisposable();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Non-disposable: " + sw.ElapsedMilliseconds.ToString());

                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbage();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Without finalizers: " + sw.ElapsedMilliseconds.ToString());

                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbageWithFinalizers();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Suppressed: " + sw.ElapsedMilliseconds.ToString());

                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                sw.Restart();
                for (var i = 0; i < garbageCount; ++i)
                    GenerateGarbageFinalizing();
                GC.Collect();
                GC.WaitForPendingFinalizers();
                GC.Collect();
                Console.WriteLine("Finalizing: " + sw.ElapsedMilliseconds.ToString());

                Console.WriteLine();
            }

            Console.ReadLine();
        }



        private static void GenerateGarbageNondisposable()
        {
            var bla = new NondisposableClass();
        }

        private static void GenerateGarbage()
        {
            var bla = new UnfinalizedClass();
            bla.Dispose();
        }

        private static void GenerateGarbageWithFinalizers()
        {
            var bla = new FinalizedClass();
            bla.Dispose();
        }

        private static void GenerateGarbageFinalizing()
        {
            var bla = new FinalizedClass();
        }



        private class NondisposableClass
        {
            private bool disposedValue = false;
        }

        private class UnfinalizedClass : IDisposable
        {
            private bool disposedValue = false;

            protected virtual void Dispose(bool disposing)
            {
                if (!disposedValue)
                {
                    if (disposing)
                    {
                    }

                    disposedValue = true;
                }
            }

            public void Dispose()
            {
                Dispose(true);
            }
        }

        private class FinalizedClass : IDisposable
        {
            private bool disposedValue = false;

            protected virtual void Dispose(bool disposing)
            {
                if (!disposedValue)
                {
                    if (disposing)
                    {
                    }

                    disposedValue = true;
                }
            }

            ~FinalizedClass()
            {
                Dispose(false);
            }

            public void Dispose()
            {
                Dispose(true);
                GC.SuppressFinalize(this);
            }
        }
    }
}