新聞中心
Intro
目前看到的一些類庫中其實很多并沒有考慮使用密封類,如果你的類型是不希望被繼承的,或者不需要被重寫的,那么就應(yīng)該考慮聲明為密封類,尤其是對于類庫項目的作者來說,這其實是非常值得考慮的一件事情,很多優(yōu)秀的類庫都會考慮這樣的問題,尤其是 .NET 框架里的一些代碼,大家看開源項目源碼的時候也可以留意一下。

Preface
默認情況下,類是不密封的。這意味著你可以從它們那里繼承。我認為這并不是正確的默認行為。事實上,除非一個類被設(shè)計成可以繼承,否則它應(yīng)該被密封。如果有需要,你仍然可以在以后刪除 sealed 修飾符。除了不是最好的默認值之外,它還會影響性能。
事實上,當一個類被密封時,JIT可以進行一些優(yōu)化,并稍微提升應(yīng)用程序的性能。
在 .NET 7 中應(yīng)該會有一個新的分析器來檢測可以被密封的類。在這篇文章中,我將展示這個 issue https://github.com/dotnet/runtime/issues/49944 中提到的密封類的一些性能優(yōu)勢。
性能優(yōu)勢
虛方法調(diào)用
當調(diào)用虛方法時,實際的方法是在運行時根據(jù)對象的實際類型找到的。每個類型都有一個虛擬方法表(vtable),其中包含所有虛擬方法的地址。這些指針在運行時被用來調(diào)用適當?shù)姆椒▽崿F(xiàn)(動態(tài)執(zhí)行)。
如果JIT知道對象的實際類型,它可以跳過vtable,直接調(diào)用正確的方法以提高性能。使用密封類型有助于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() { }
}
|
方法 |
算術(shù)平均值 |
誤差 |
方差 |
中位數(shù) |
比率 |
代碼大小 |
|
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 可以確定實際類型時,即使類型沒有密封,它也可以使用直接調(diào)用。例如,以下兩個片段之間沒有區(qū)別:
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
}
對象類型轉(zhuǎn)換 (is / as)
當對象類型轉(zhuǎn)換時,CLR 必須在運行時檢查對象的類型。當轉(zhuǎn)換到一個非密封的類型時,運行時必須檢查層次結(jié)構(gòu)中的所有類型。然而,當轉(zhuǎn)換到一個密封的類型時,運行時必須只檢查對象的類型,所以它的速度更快。
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 {}
|
方法 |
平均值 |
誤差 |
方差 |
中位數(shù) |
|
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 |
數(shù)組 Arrays
.NET中的數(shù)組是支持協(xié)變的。這意味著,BaseType[] value = new DerivedType[1] 是有效的。而其他集合則不是這樣的。例如,List value = new List(); 是無效的。
協(xié)變會帶來性能上的損失。事實上,JIT在將一個項目分配到數(shù)組之前必須檢查對象的類型。當使用密封類型時,JIT可以取消檢查。你可以查看 Jon Skeet 的文章 https://codeblog.jonskeet.uk/2013/06/22/array-covariance-not-just-ugly-but-slow-too/ 來獲得更多關(guān)于性能損失的細節(jié)。
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 { }
方法平均值誤差方
|
方法 |
平均值 |
誤差 |
方差 |
中位數(shù) |
比率 |
|
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 |
數(shù)組轉(zhuǎn)換成 Span
你可以將數(shù)組轉(zhuǎn)換為 Span 或 ReadOnlySpan。出于與前面部分相同的原因,JIT在將數(shù)組轉(zhuǎn)換為 Span 之前必須檢查對象的類型。當使用一個密封的類型時,可以避免檢查并稍微提高性能。
public class SealedBenchmark
{
SealedType[] sealedTypeArray = new SealedType[100];
NonSealedType[] nonSealedTypeArray = new NonSealedType[100];
[Benchmark(Baseline = true)]
public SpanNonSealed() => nonSealedTypeArray;
[Benchmark]
public SpanSealed() => sealedTypeArray;
}
public class BaseType {}
public class NonSealedType : BaseType { }
public sealed class SealedType : BaseType { }
|
方法 |
平均值 |
誤差 |
方差 |
中位數(shù) |
比率 |
|
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 |
檢測不可達的代碼
當使用密封類型時,編譯器知道一些轉(zhuǎn)換是無效的。所以,它可以報告警告和錯誤。這可能會減少你的應(yīng)用程序中的錯誤,同時也會刪除不可到達的代碼。
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 包含一個規(guī)則,可以檢查可能被密封的類型。
dotnet add package Meziantou.Analyzer
它應(yīng)該使用 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中,這應(yīng)該是 CA1851 的標準靜態(tài)分析的一部分 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
從上面的解釋和基準測試中我們可以看到一些密封類為我們帶來的好處,我們在設(shè)計一個類型的時候就應(yīng)該去考慮這個類型是不是允許被繼承,如果不允許被繼承,則應(yīng)該考慮將其聲明為 sealed,如果你有嘗試過 Sonar Cloud 這樣的靜態(tài)代碼分析工具,你也會發(fā)現(xiàn),有一些 private 的類型如果沒有聲明為 sealed 就會被報告為 Code Smell 一個代碼中的壞味道
除了性能上的好處,首先將一個類型聲明為 sealed 可以實現(xiàn)更好的 API 兼容性,如果從密封類變成一個非密封類不是一個破壞性的變更,但是從一個非密封類變成一個密封類是一個破壞性的變更
希望大家在自己的類庫項目中新建類型的時候會思考一下是否該將其聲明為 sealed,除此之外可以不 public 的類型可以聲明為 internal,不 public 不必要的類型,希望有越來越多更好更高質(zhì)量的開源項目
原文地址:https://www.meziantou.net/performance-benefits-of-sealed-class.htm
文章名稱:.NET中密封類的性能優(yōu)勢,你知道幾個?
網(wǎng)址分享:http://www.dlmjj.cn/article/cdpdjcg.html


咨詢
建站咨詢
