当前位置: 首页 > 科技观察

关于.NET中的密封类,您知道多少性能优势?

时间:2023-03-13 02:14:07 科技观察

Intro最近看到一篇文章Performancebenefitsofsealedclassin.NET,觉得写的不错,翻译一下,分享给大家。其实目前我看到的很多类库都没有考虑使用密封类。如果你的类型不想被继承,或者不需要重写,那么你应该考虑将它声明为密封类,尤其是类库。对于项目的作者来说,这其实是一件值得考虑的事情。很多优秀的类库都会考虑这样的问题,尤其是.NET框架中的一些代码。大家在看开源项目源码的时候也可以关注一下。默认情况下,前言类是不密封的。这意味着您可以继承它们。我认为这不是正确的默认行为。事实上,除非一个类被设计成可以继承的,否则它应该是密封的。如果需要,您仍然可以稍后移除密封修饰符。除了不是最佳默认设置外,它还会影响性能。事实上,当一个类被密封时,JIT可以做一些优化,稍微提高应用程序的性能。在.NET7中应该有一个新的分析器来检测可以密封的类。在本文中,我将展示本期https://github.com/dotnet/runtime/issues/49944中提到的密封类的一些性能优势。性能优势虚拟方法调用当调用虚拟方法时,会在运行时根据对象的实际类型找到实际方法。每种类型都有一个虚方法表(vtable),里面包含了所有虚方法的地址。这些指针在运行时用于调用适当的方法实现(动态执行)。如果JIT知道对象的实际类型,它可以跳过vtable并直接调用正确的方法以提高性能。使用密封类型有助于JIT,因为它知道不能有任何派生类。公共类SealedBenchmark{只读NonSealedTypenonSealedType=new();只读SealedTypesealedType=new();[Benchmark(Baseline=true)]publicvoidNonSealed(){//JIT无法知道nonSealedType的实际类型。实际上,//它可能已通过另一种方法设置为派生类。//所以,为了安全,它必须使用虚拟调用。nonSealedType.Method();}[Benchmark]publicvoidSealed(){//JIT确定sealedType是一个SealedType。由于该类是密封的,//它不能是派生类型的实例。//所以它可以使用更快的直接调用。sealedType.Method();}}internalclassBaseType{publicvirtualvoidMethod(){}}internalclassNonSealedType:BaseType{publicoverridevoidMethod(){}}internalsealedclassSealedType:BaseType{publicoverridevoidMethod(){}}方法计算平均值错误方差中位数比代码大小NonSealed0.4465ns0.0276ns0.0258ns0.4437ns1.0018BSealed0.0107ns0.0160ns0.0150ns0.0000ns0.027B请注意,当JIT可以确定实际类型时,即使类型未密封,也可以使用直接调用。例如,下面两个片段之间没有区别:voidNonSealed(){varinstance=newNonSealedType();实例.方法();//JIT知道`instance`是NonSealedType因为它在方法中设置//并且从未修改过,所以它使用直接调用}voidSealed(){varinstance=newSealedType();实例.方法();//JIT知道instance是SealedType,所以直接调用}对象类型转换(is/as)当对象类型转换时,CLR必须在运行时检查对象的类型。转换为未密封类型时,运行时必须检查层次结构中的所有类型。但是,当转换为密封类型时,运行时必须只检查对象的类型,因此速度更快。公共类SealedBenchmark{只读BaseTypebaseType=new();[基准(基线=真)]publicboolIs_Sealed()=>baseType是SealedType;[基准测试]publicboolIs_NonSealed()=>baseTypeisNonSealedType;}internalclassBaseType{}internalclassNonSealedType:BaseType{}internalsealedclassSealedType:BaseType{}方法平均误差方差中位数Is_NonSealed1.6560ns0.0223ns0.0208ns1.00Is_Sealed0.1505ns0.0221ns0.0207ns0.09阵列。NET数组是协变的。这意味着,BaseType[]value=newDerivedType[1]是有效的。这不是其他集合的情况。例如,Listvalue=newList();是无效的。协变会带来性能损失。事实上,JIT在将项目分配给数组之前必须检查对象的类型。使用密封类型时,JIT可以取消检查。您可以查看JonSkeet的文章https://codeblog.jonskeet.uk/2013/06/22/array-covariance-not-just-ugly-but-slow-too/了解有关性能损失的更多详细信息。NonSealedType[]nonSealedTypeArray=newNonSealedType[100];[基准(基线=真)]publicvoidNonSealed(){nonSealedTypeArray[0]=newNonSealedType();}[基准]publicvoidSealed(){sealedTypeArray[0]=newSealedType();}}internalclassBaseType{}internalclassNonSealedType:BaseType{}internalsealedclassSealedType:BaseType{}方法均方差方法均方差中位数比NonSealed3.420ns0.0897ns0.0881ns1.0044BSealed2.951ns0.0781ns0.0802ns0.8658B数组转换为Span您可以将数组转换为Span或ReadOnlySpan。出于与上一节相同的原因,JIT必须在将数组转换为跨度之前检查对象的类型。使用密封型时,可以避免检查并略微提高性能。公共类SealedBenchmark{SealedType[]sealedTypeArray=newSealedType[100];NonSealedType[]nonSealedTypeArray=newNonSealedType[100];[基准(基线=真)]publicSpanNonSealed()=>nonSealedTypeArray;SpanSealed()=>sealedTypeArray;}publicclassBaseType{}publicclassNonSealedType:BaseType{}publicsealedclassSealedType:BaseType{}方法均值误差方差中值比NonSealed0.0668ns0.0156ns0.0138ns1.0064BSealed0.0307ns0.0209ns0.0185ns0.5035B检测无法访问的代码当使用密封类型时,编译器知道某些转换是无效的。因此,它可以报告警告和错误。这可能会减少应用程序中的错误,同时还会删除无法访问的代码。classSample{publicvoidFoo(NonSealedTypeobj){_=objasIMyInterface;//好的,因为派生类可以实现接口}publicvoidFoo(SealedTypeobj){_=objisIMyInterface;//??警告CS0184_=objasIMyInterface;//?错误CS0039}}publicclassNonSealedType{}publicsealedclassSealedType{}publicinterfaceIMyInterface{}查找可以密封的类型Meziantou.Analyzer包含一个可以检查可以密封的类型的规则。dotnet添加包Meziantou.Analyzer,它应该报告可以使用MA0053密封的任何内部类型:您还可以通过编辑.editorconfig文件指示分析器报告公共类型。[*.cs]dotnet_diagnostic.MA0053.severity=suggestion#Reportpublicclasseswithoutinheritances(default:false)MA0053.public_class_should_be_sealed=true#Reportclasswithoutinheritancesevenifthereisvirtualmembers(default:false)MA0053.class_with_mber_virtual类似dotnet的工具格式可以用来解决这个问题。dotnetformatanalyzers--severityinfo注意:在.NET7中,这应该是CA1851标准静态分析的一部分https://github.com/dotnet/roslyn-analyzers/pull/5594补充所有基准测试都是使用Runningwiththe以下配置:BenchmarkDotNet=v0.13.1,OS=Windows10.0.22000AMDRyzen75800X,1个CPU,16个逻辑内核和8个物理内核。NETSDK=7.0.100-preview.2.22153.17[主机]:.NET6.0.3(6.0.322.12309),X64RyuJITDefaultJob:.NET6.0.3(6.0.322.12309),X64RyuJIT其他资源为什么密封了这么多的框架类?AnalyzerProposal:Sealinternal/privatetypesMorefromtheexplanationaboveandInthe基准测试,我们可以看到一些密封类给我们带来的好处。我们在设计一个类型的时候,应该考虑这个类型是否允许被继承。如果不允许被继承,我们应该考虑将其声明为sealed,如果你尝试过像SonarCloud这样的静态代码分析工具,你也会发现一些私有类型如果不声明为CodeSmell会报错密封。代码中的难闻气味除了性能优势之外,首先将类型声明为密封允许更好的API兼容性,如果从密封类到非密封类不是重大更改,而是从非密封类到密封类是一个突破性的改变Hope当你在自己的类库项目中创建一个新的类型时,你会考虑是否应该将它声明为密封的。另外,非public的类型可以声明为internal,非public的类型是不必要的。希望有越来越多更好质量更高的开源项目原文地址:https://www.meziantou.net/performance-benefits-of-sealed-class.htm