拥有 Finalizer 的开销 - with/without 在 Dispose 中 SuppressFinalize
Overhead of having a Finalizer - with/without SuppressFinalize in Dispose
假设如下:
- A class 只管理了成员。
- 一些成员实施
IDisposable
.
- class 是
sealed
- class 无法从非托管资源派生和添加。
- 该对象在
using
语句中使用 - 即 Dispose()
在完成时调用。
class 有 3 种可能的 IDisposable
实现:
- 在
IDisposable
成员上调用 Dispose()
的最小 Dispose
方法 - 无终结器。
- 带有终结器的标准
IDisposable
实现 缺少 Dispose()
. 中通常的 GC.SuppressFinalize(this)
调用
- 带有终结器的完整标准
IDisposable
实现(以及 Dispose()
中的 GC.SuppressFinalize(this)
调用)。
下列说法正确的是?我理解正确吗?
- 情况 A. 比 B. 和 C. 的开销略小,因为对象没有终结器,所以它不会进入 GC 终结队列 - 因此 GC 可以在早期清除该对象收集 - 无开销。
- 情况 B。该对象有一个终结器,因此将在 GC 终结器队列中结束,并且终结器将被调用(因为它没有被抑制)- 终结器调用 dispose 什么都不做,因为它已经被调用了。这会导致对象在终结器队列中的开销很小,并且终结器调用的开销也很小。
- 情况 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"。
关于编号问题:
- 是的,有一个终结器 运行 会在收集时产生一些开销
- 是,在已处置的对象上调用
Dispose()
should be ignored
- 这些 类 在创建实例时被添加到终结队列,但在 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 永远不要实施 Finalize
。 Finalize
方法有一些合法用途,但是重写它的 类 应该避免持有对终结不需要的任何内容的引用(有点烦人,如果可终结的 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);
}
}
}
}
假设如下:
- A class 只管理了成员。
- 一些成员实施
IDisposable
. - class 是
sealed
- class 无法从非托管资源派生和添加。 - 该对象在
using
语句中使用 - 即Dispose()
在完成时调用。
class 有 3 种可能的 IDisposable
实现:
- 在
IDisposable
成员上调用Dispose()
的最小Dispose
方法 - 无终结器。 - 带有终结器的标准
IDisposable
实现 缺少Dispose()
. 中通常的 - 带有终结器的完整标准
IDisposable
实现(以及Dispose()
中的GC.SuppressFinalize(this)
调用)。
GC.SuppressFinalize(this)
调用
下列说法正确的是?我理解正确吗?
- 情况 A. 比 B. 和 C. 的开销略小,因为对象没有终结器,所以它不会进入 GC 终结队列 - 因此 GC 可以在早期清除该对象收集 - 无开销。
- 情况 B。该对象有一个终结器,因此将在 GC 终结器队列中结束,并且终结器将被调用(因为它没有被抑制)- 终结器调用 dispose 什么都不做,因为它已经被调用了。这会导致对象在终结器队列中的开销很小,并且终结器调用的开销也很小。
- 情况 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"。
关于编号问题:
- 是的,有一个终结器 运行 会在收集时产生一些开销
- 是,在已处置的对象上调用
Dispose()
should be ignored - 这些 类 在创建实例时被添加到终结队列,但在 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 永远不要实施 Finalize
。 Finalize
方法有一些合法用途,但是重写它的 类 应该避免持有对终结不需要的任何内容的引用(有点烦人,如果可终结的 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);
}
}
}
}