Intro
最近看到一篇文章 Performance benefits of sealed class in .NET,觉得写得不错,翻译一下,分享给大家。
目前看到的一些类库中其实很多并没有考虑使用密封类,如果你的类型是不希望被继承的,或者不需要被重写的,那么就应该考虑声明为密封类,尤其是对于类库项目的作者来说,这其实是非常值得考虑的一件事情,很多优秀的类库都会考虑这样的问题,尤其是 .NET 框架里的一些代码,大家看开源项目源码的时候也可以留意一下。
Preface
默认情况下,类是不密封的。这意味着你可以从它们那里继承。我认为这并不是正确的默认行为。事实上,除非一个类被设计成可以继承,否则它应该被密封。如果有需要,你仍然可以在以后删除 sealed 修饰符。除了不是最好的默认值之外,它还会影响性能。
事实上,当一个类被密封时,JIT可以进行一些优化,并稍微提升应用程序的性能。
在 .NET 7 中应该会有一个新的分析器来检测可以被密封的类。在这篇文章中,我将展示这个 issue https://github.com/dotnet/runtime/issues/49944 中提到的密封类的一些性能优势。
性能优势
虚方法调用
当调用虚方法时,实际的方法是在运行时根据对象的实际类型找到的。每个类型都有一个虚拟方法表(vtable),其中包含所有虚拟方法的地址。这些指针在运行时被用来调用适当的方法实现(动态执行)。
如果JIT知道对象的实际类型,它可以跳过vtable,直接调用正确的方法以提高性能。使用密封类型有助于JIT,因为它知道不能有任何派生类。
public class SealedBenchmark
{
readonly NonSealedType nonSealedType = new();
readonly SealedType sealedType = new();
[Benchmark(Baseline = true)]
public void NonSealed()
{
// The JIT cannot know the actual type of nonSealedType. Indeed,
// it could have been set to a derived class by another method.
// So, it must use a virtual call to be safe.
nonSealedType.Method();
}
[Benchmark]
public void Sealed()
{
// The JIT is sure sealedType is a SealedType. As the class is sealed,
// it cannot be an instance from a derived type.
// So it can use a direct call which is faster.
sealedType.Method();
}
}
internal class BaseType
{
public virtual void Method() { }
}
internal class NonSealedType : BaseType
{
public override void Method() { }
}
internal sealed class SealedType : BaseType
{
public override void Method() { }
}
方法 | 算术平均值 | 误差 | 方差 | 中位数 | 比率 | 代码大小 |
NonSealed | 0.4465 ns | 0.0276 ns | 0.0258 ns | 0.4437 ns | 1.00 | 18 B |
Sealed | 0.0107 ns | 0.0160 ns | 0.0150 ns | 0.0000 ns | 0.02 | 7 B |
请注意,当 JIT 可以确定实际类型时,即使类型没有密封,它也可以使用直接调用。例如,以下两个片段之间没有区别:
void NonSealed()
{
var instance = new NonSealedType();
instance.Method(); // The JIT knows `instance` is NonSealedType because it is set
// in the method and never modified, so it uses a direct call
}
void Sealed()
{
var instance = new SealedType();
instance.Method(); // The JIT knows instance is SealedType, so it uses a direct call
}
对象类型转换 (is / as)
当对象类型转换时,CLR 必须在运行时检查对象的类型。当转换到一个非密封的类型时,运行时必须检查层次结构中的所有类型。然而,当转换到一个密封的类型时,运行时必须只检查对象的类型,所以它的速度更快。
public class SealedBenchmark
{
readonly BaseType baseType = new();
[Benchmark(Baseline = true)]
public bool Is_Sealed() => baseType is SealedType;
[Benchmark]
public bool Is_NonSealed() => baseType is NonSealedType;
}
internal class BaseType {}
internal class NonSealedType : BaseType {}
internal sealed class SealedType : BaseType {}
方法 | 平均值 | 误差 | 方差 | 中位数 |
Is_NonSealed | 1.6560 ns | 0.0223 ns | 0.0208 ns | 1.00 |
Is_Sealed | 0.1505 ns | 0.0221 ns | 0.0207 ns | 0.09 |
数组 Arrays
.NET中的数组是支持协变的。这意味着,BaseType[] value = new DerivedType[1] 是有效的。而其他集合则不是这样的。例如,List value = new List(); 是无效的。
协变会带来性能上的损失。事实上,JIT在将一个项目分配到数组之前必须检查对象的类型。当使用密封类型时,JIT可以取消检查。你可以查看 Jon Skeet 的文章 https://codeblog.jonskeet.uk/2013/06/22/array-covariance-not-just-ugly-but-slow-too/ 来获得更多关于性能损失的细节。
NonSealedType[] nonSealedTypeArray = new NonSealedType[100];
[Benchmark(Baseline = true)] public void NonSealed() { nonSealedTypeArray[0] =
new NonSealedType(); } [Benchmark] public void Sealed() { sealedTypeArray[0] =
new SealedType(); }}internal class BaseType { }internal class NonSealedType :
BaseType { }internal sealed class SealedType : BaseType { }
方法平均值误差方
方法 | 平均值 | 误差 | 方差 | 中位数 | 比率 |
NonSealed | 3.420 ns | 0.0897 ns | 0.0881 ns | 1.00 | 44 B |
Sealed | 2.951 ns | 0.0781 ns | 0.0802 ns | 0.86 | 58 B |
数组转换成 Span
你可以将数组转换为 Span 或 ReadOnlySpan。出于与前面部分相同的原因,JIT在将数组转换为 Span 之前必须检查对象的类型。当使用一个密封的类型时,可以避免检查并稍微提高性能。
public class SealedBenchmark
{
SealedType[] sealedTypeArray = new SealedType[100];
NonSealedType[] nonSealedTypeArray = new NonSealedType[100];
[Benchmark(Baseline = true)]
public Span<NonSealedType> NonSealed() => nonSealedTypeArray;
[Benchmark]
public Span<SealedType> Sealed() => sealedTypeArray;
}
public class BaseType {}
public class NonSealedType : BaseType { }
public sealed class SealedType : BaseType { }
方法 | 平均值 | 误差 | 方差 | 中位数 | 比率 |
NonSealed | 0.0668 ns | 0.0156 ns | 0.0138 ns | 1.00 | 64 B |
Sealed | 0.0307 ns | 0.0209 ns | 0.0185 ns | 0.50 | 35 B |
检测不可达的代码
当使用密封类型时,编译器知道一些转换是无效的。所以,它可以报告警告和错误。这可能会减少你的应用程序中的错误,同时也会删除不可到达的代码。
class Sample
{
public void Foo(NonSealedType obj)
{
_ = obj as IMyInterface; // ok because a derived class can implement the interface
}
public void Foo(SealedType obj)
{
_ = obj is IMyInterface; // ⚠️ Warning CS0184
_ = obj as IMyInterface; // ❌ Error CS0039
}
}
public class NonSealedType { }
public sealed class SealedType { }
public interface IMyInterface { }
寻找可以被密封的类型
Meziantou.Analyzer 包含一个规则,可以检查可能被密封的类型。
dotnet add package Meziantou.Analyzer
它应该使用 MA0053 报告任何可以被密封的internal 类型:
你也可以通过编辑 .editorconfig文件指示分析器报告 public类型。
[*.cs]
dotnet_diagnostic.MA0053.severity = suggestion
# Report public classes without inheritors (default: false)
MA0053.public_class_should_be_sealed = true
# Report class without inheritors even if there is virtual members (default: false)
MA0053.class_with_virtual_member_shoud_be_sealed = true
你可以使用像 dotnet format 这样的工具来解决这个问题。
dotnet format analyzers --severity info
注意:在.NET 7中,这应该是 CA1851 的标准静态分析的一部分 https://github.com/dotnet/roslyn-analyzers/pull/5594
补充说明
所有的基准都是使用以下配置运行的:
BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
AMD Ryzen 7 5800X, 1 CPU, 16 logical and 8 physical cores
.NET SDK=7.0.100-preview.2.22153.17
[Host] : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
DefaultJob : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
其他资源
- Why Are So Many Of The Framework Classes Sealed?
- Analyzer Proposal: Seal internal/private types
More
从上面的解释和基准测试中我们可以看到一些密封类为我们带来的好处,我们在设计一个类型的时候就应该去考虑这个类型是不是允许被继承,如果不允许被继承,则应该考虑将其声明为 sealed,如果你有尝试过 Sonar Cloud 这样的静态代码分析工具,你也会发现,有一些 private 的类型如果没有声明为 sealed 就会被报告为 Code Smell 一个代码中的坏味道
除了性能上的好处,首先将一个类型声明为 sealed 可以实现更好的 API 兼容性,如果从密封类变成一个非密封类不是一个破坏性的变更,但是从一个非密封类变成一个密封类是一个破坏性的变更
希望大家在自己的类库项目中新建类型的时候会思考一下是否该将其声明为 sealed,除此之外可以不 public 的类型可以声明为 internal,不 public 不必要的类型,希望有越来越多更好更高质量的开源项目
原文地址:https://www.meziantou.net/performance-benefits-of-sealed-class.htm