.NET 中密封类的性能优势

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() { }
}
方法算术平均值误差方差中位数比率代码大小
NonSealed0.4465 ns0.0276 ns0.0258 ns0.4437 ns1.0018 B
Sealed0.0107 ns0.0160 ns0.0150 ns0.0000 ns0.027 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_NonSealed1.6560 ns0.0223 ns0.0208 ns1.00
Is_Sealed0.1505 ns0.0221 ns0.0207 ns0.09

数组 Arrays

.NET中的数组是支持协变的。这意味着,BaseType[] value = new DerivedType[1]是有效的。而其他集合则不是这样的。例如,List<BaseType> value = new List<DerivedType>();是无效的。

协变会带来性能上的损失。事实上,JIT在将一个项目分配到数组之前必须检查对象的类型。当使用密封类型时,JIT可以取消检查。你可以查看 Jon Skeet 的文章 https://codeblog.jonskeet.uk/2013/06/22/array-covariance-not-just-ugly-but-slow-too/ 来获得更多关于性能损失的细节。

public class SealedBenchmark
{
    SealedType[] sealedTypeArray = new SealedType[100];
    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 { }
方法平均值误差方差中位数比率
NonSealed3.420 ns0.0897 ns0.0881 ns1.0044 B
Sealed2.951 ns0.0781 ns0.0802 ns0.8658 B

数组转换成 Span

你可以将数组转换为 Span<T>ReadOnlySpan<T>。出于与前面部分相同的原因,JIT在将数组转换为 Span<T> 之前必须检查对象的类型。当使用一个密封的类型时,可以避免检查并稍微提高性能。

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 { }
方法平均值误差方差中位数比率
NonSealed0.0668 ns0.0156 ns0.0138 ns1.0064 B
Sealed0.0307 ns0.0209 ns0.0185 ns0.5035 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

More

从上面的解释和基准测试中我们可以看到一些密封类为我们带来的好处,我们在设计一个类型的时候就应该去考虑这个类型是不是允许被继承,如果不允许被继承,则应该考虑将其声明为 sealed,如果你有尝试过 Sonar Cloud 这样的静态代码分析工具,你也会发现,有一些 private 的类型如果没有声明为 sealed 就会被报告为 Code Smell 一个代码中的坏味道。

除了性能上的好处,首先将一个类型声明为 sealed 可以实现更好的 API 兼容性。从密封类变成一个非密封类不是一个破坏性的变更,但是从一个非密封类变成一个密封类是一个破坏性的变更。

希望大家在自己的类库项目中新建类型的时候会思考一下是否该将其声明为 sealed,除此之外可以不 public 的类型可以声明为 internal,除非必要不要将类型声明为 public

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值