.NET 中密封类的性能优势,你知道几个?

发表于:2022-3-30 09:22

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:WeihanLi    来源:amazingdotnet

  Intro
  最近看到一篇文章 Performance benefits of sealed class in .NET,觉得写得不错,翻译一下,分享给大家。
  目前看到的一些类库中其实很多并没有考虑使用密封类,如果你的类型是不希望被继承的,或者不需要被重写的,那么就应该考虑声明为密封类,尤其是对于类库项目的作者来说,这其实是非常值得考虑的一件事情,很多优秀的类库都会考虑这样的问题,尤其是 .NET 框架里的一些代码,大家看开源项目源码的时候也可以留意一下。
  Preface
  默认情况下,类是不密封的。这意味着你可以从它们那里继承。我认为这并不是正确的默认行为。事实上,除非一个类被设计成可以继承,否则它应该被密封。如果有需要,你仍然可以在以后删除 sealed 修饰符。除了不是最好的默认值之外,它还会影响性能。
  事实上,当一个类被密封时,JIT可以进行一些优化,并稍微提升应用程序的性能。
  在 .NET 7 中应该会有一个新的分析器来检测可以被密封的类。。
  性能优势
  虚方法调用
  当调用虚方法时,实际的方法是在运行时根据对象的实际类型找到的。每个类型都有一个虚拟方法表(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() { }
  }


  请注意,当 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 {}


  数组 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 { }
  方法平均值误差方


  数组转换成 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 { }


  检测不可达的代码
  当使用密封类型时,编译器知道一些转换是无效的。所以,它可以报告警告和错误。这可能会减少你的应用程序中的错误,同时也会删除不可到达的代码。
  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

  补充说明
  所有的基准都是使用以下配置运行的:
  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 不必要的类型,希望有越来越多更好更高质量的开源项目。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号