为什么要把类设置成密封?

为什么,设置成,密封 · 浏览次数 : 718

小编点评

**生成内容时需要带简单的排版** **1. 摘要** * 包括主要性能提升方法的概述。 * 例如,使用 APM 快速分析性能瓶颈。 **2. 性能提升方法** * 例如,使用 Dotnet Tools 进行性能分析。 * 包括使用 JIT 的优化。 * 例如,优化数组类型转换效率。 **3. 性能分析工具** * 包括使用 APM、dotnet tools 等工具进行性能分析。 * 例如,使用 APM 快速分析性能瓶颈。 **4. 性能陷阱** * 例如,垃圾回收器、JIT 的优化。 * 包括如何优化这些性能陷阱。 **5. 性能分析优化** * 例如,使用 Dotnet Tools 进行性能分析。 * 包括如何优化这些性能陷阱。 **6. 性能分析工具** * 包括使用 APM、dotnet tools 等工具进行性能分析。 * 例如,使用 APM 快速分析性能瓶颈。

正文

前几天笔者提交了关于FasterKvCache的性能优化代码,其中有一个点就是我把一些后续不需要继承的类设置为了sealed密封类,然后就有小伙伴在问,为啥这个地方需要设置成sealed

提交的代码如下所示:

一般业务开发的同学可能接触密封类比较少,密封类除了框架设计约束(不能被继承)以外,还有一个微小的性能提升,不过虽然它是一个微小的优化点,多框架开发的作者都会做这样的优化,如果方法调用的频次很高,那也会带来很大的收益。

笔者最开始是从.NET runtime 中的代码学习到这一个优化技巧,后面有看到meziantou大佬的文章performance-benefits-of-sealed-class完整的学习了一下。

然后本来是想翻译一下这篇文章,找了下发现 Weihan 大佬今年年初翻译了meziantou大佬的文章,质量非常高的中文版,大家可以戳链接看看,既然如此在本文中带大家回顾一下文章中例子,另外从 JIT ASM 的层面分析为什么性能会有提升。

性能优势

虚方法调用

在上面提到的文章例子中,有一个虚方法的调用,大家其实要明白一点,现在面向对象的封装、继承、多态中的多态实现主要就是靠虚方法。

一个类型可能会有子类,子类可能会重写类型的方法从而达到不同的行为(多态),而这些重写的方法都在虚方法表里,调用的话就需要查表。

回到文中的代码,大佬构建了一个这样的测试用例:

public class SealedBenchmark
{
    readonly NonSealedType nonSealedType = new();
    readonly SealedType sealedType = new();

    [Benchmark(Baseline = true)]
    public void NonSealed()
    {
        // JIT不能知道nonSealedType的实际类型.
        // 它可能已经被另一个方法设置为派生类。
        // 所以,为了安全起见,它必须使用一个虚拟调用。
        nonSealedType.Method();
    }

    [Benchmark]
    public void Sealed()
    {
        // JIT确信sealedType是一个SealedType。 由于该类是密封的。
        // 它不可能是一个派生类型的实例。
        // 所以它可以使用直接调用,这样会更快。
        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() { }
}

取得的结果就是密封类要比非密封的快 98%。

那么为什么会这样呢?首先我们来比较一下两个方法的 IL 代码,发现是一模一样的,对于方法调用都是用了callvirt(它就是用来调用虚方法的,想了解更多详情可以看这里),因为 instance 是从字段中加载的,编译器无法知道具体的类型,只能使用callvirt

那区别在哪里呢?我们可以看到 JIT 生成后的汇编代码,可以很清楚的看到密封类少了两条指令,因为 JIT 可以从密封类中知道它不可能被继承,也不可能被重写,所以是直接跳转到密封类目标方法执行,而非密封类还有一个查表的过程。而现在很多大佬聊天说 JIT 的"去虚拟化"其实主要就是在 JIT 编译时去除了callvirt调用。

另外文中也提到了一段代码,如果 JIT 能确定类型,也是直接调用的:

void NonSealed()
{
    var instance = new NonSealedType();
    instance.Method(); // JIT知道`instance`是NonSealedType,因为它是在方法中被创建的,
                       // 从未被修改过,所以它使用直接调用
}

void Sealed()
{
    var instance = new SealedType();
    instance.Method(); // JIT知道类型是SealedType, 所以直接调用
}

此时两者的汇编代码没有任何区别,都是直接 jmp 到目标方法。

发现一个有趣的东西,如果我们切到.NET Framework 的 JIT,可以发现.NET Framework 的 JIT 没有.NET 生成的这么高效,没有直接 jmp 到目标方法,而是多了一层 call 和 ret。所以,朋友们还等什么呢?快升级.NET 版本吧。

对象类型转换 (is / as)

同样有下面这样一段代码,测试密封类和非密封类的对象类型转换性能:

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 {}

毫无疑问,密封类快 91%。

IL 层面,两个方法都是一模一样:

可以看到密封类的代码相当高效,直接比较一下就转换类型返回了,而非密封类还需要 call 方法走查表流程:

数组

.NET 的数组是协变的,协变兼容的话就意味着在添加进入数组时需要检查它的类型,而如果是密封类那就可以删除检查,同样有下面一段代码:

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 { }

密封类的性能要高 14%左右。

打开 IL 代码,两者编译出的方法都是一样的,但是跳转到汇编代码可以发现差别,同样的是Stelem.Ref给数组赋值,密封类只是检查了一下数组长度,然后直接赋值,而非密封类还需要调用System.Runtime.CompilerServices.CastHelpers.StelemRef进行检查才能完成赋值。

将数组转换为Span<T>

和数组一样,将数组转换为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 { }

密封类的性能要高 50%:

同样,这也是 IL 一模一样的,在 JIT 阶段做的优化,可以明显的看到,JIT 为非密封类单独做了类型检查:

总结

笔者在 FasterKvCache 代码中将一些类设置为sealed的原因显而易见:

  • 为了让类的职责更加清晰,在设计中没有计划让它有派生类
  • 为了性能的提升,JIT 优化可以让其方法调用更快

还有更多有趣的东西(比如 IDE 智能提示将类设置为密封,如何使用 dotnet format 集成这些分析),大家可以翻阅原文或者 Weihan 大佬翻译的文章。

.NET性能优化交流群

相信大家在开发中经常会遇到一些性能问题,苦于没有有效的工具去发现性能瓶颈,或者是发现瓶颈以后不知道该如何优化。之前一直有读者朋友询问有没有技术交流群,但是由于各种原因一直都没创建,现在很高兴的在这里宣布,我创建了一个专门交流.NET性能优化经验的群组,主题包括但不限于:

  • 如何找到.NET性能瓶颈,如使用APM、dotnet tools等工具
  • .NET框架底层原理的实现,如垃圾回收器、JIT等等
  • 如何编写高性能的.NET代码,哪些地方存在性能陷阱

希望能有更多志同道合朋友加入,分享一些工作中遇到的.NET性能问题和宝贵的性能分析优化经验。由于已经达到200人,可以加我微信,我拉你进群: ls1075

与为什么要把类设置成密封?相似的内容:

为什么要把类设置成密封?

前几天笔者提交了关于FasterKvCache的性能优化代码,其中有一个点就是我把一些后续不需要继承的类设置为了sealed密封类,然后就有小伙伴在问,为啥这个地方需要设置成sealed? 提交的代码如下所示: 一般业务开发的同学可能接触密封类比较少,密封类除了框架设计约束(不能被继承)以外,还有一

Beetl 源码解析:GroupTemplate 类

本文首发于公众号:腐烂的橘子 前言 Beetl 是一款 Java 模板引擎,在公司的项目中大量运用,它的作用是写通用代码时,有一些差异化的逻辑需要处理,这时可以把这些差异化的逻辑写在模板里,程序直接调用,实现了代码的低耦合。 有人问差异化的东西为什么不能通过配置实现?原因是配置只能将一些差异化的值抽

分块

首先我们要了解一个问题:为什么要用分块 我们拿一道题目举例: ### 例题 给定一个长度为 $N$ 的数列 $A$,以及 $M$ 条指令,每条指令可能是以下两种之一: 1. `C l r d`,表示把 $A[l],A[l+1],…,A[r]$ 都加上 $d$。 2. `Q l r`,表示询问数列中第

[转帖]聊一聊nginx中KeepAlive的设置

文章目录 问题分析为什么要有KeepAlive?TCP KeepAlive和HTTP的Keep-Alive是一样的吗?Nginx的TCP KeepAlive如何设置Apache中KeepAlive和KeepAliveTimeOut参考资料 问题 之前工作中遇到一个KeepAlive的问题,现在把它记

个人语录

任泽平曾问过俞敏洪,新东方为什么非得转型?俞敏洪这样回答: 「别人做过的事情,只要你去做,并且把它做得比别人更好,说不定你也能成功。前提条件就是你必须要比别人做得好,比别人更加有耐心。所以我并不认为一个企业,一定要去做别人没做过的事情。」

[转帖]让vim显示空格,tab字符,及vim多行注释

http://t.zoukankan.com/jjxhp-p-10667934.html 1、显示 TAB 键 文件中有 TAB 键的时候,你是看不见的。要把它显示出来: :set list 现在 TAB 键显示为 ^I,而 $显示在每行的结尾,以便你能找到可能会被你忽略的空白字符在哪里 这样做的一

[转帖]Vim里面显示TAB键

http://www.ekangw.net/a/diannaojiqiao/2022/0916/188768.html 1、文件中有 TAB 键的时候,你是看不见的。要把它显示出来:C代码 :set list 现在 TAB 键显示为 ^I,而 $显示在每行的结尾,以便你能找到可能会被你忽略的空白字符

Langchain-Chatchat项目:3-Langchain计算器工具Agent思路和实现

本文主要讨论Langchain-Chatchat项目中自定义Agent问答的思路和实现。以"计算器工具"为例,简单理解就是通过LLM识别应该使用的工具类型,然后交给相应的工具(也是LLM模型)来解决问题。一个LLM模型可以充当不同的角色,要把结构化的Prompt模板写好,充分利用LLM的Zero/O

C#的关于窗体的类库方案 - 开源研究系列文章

这次想到了以前编写的关于应用的那个类库,不过当时的只是定义了显示接口,然后调用窗体显示。现在想到要把这个关于窗体的类库进行集合,统一调用,于是就把原来的代码进行了修改完善,终于得到了这次这个例子。 这个例子主要实现了4种关于窗体的形式。第1种为普通的显示文件的信息(即程序集信息里的那些信息);第2种

iOS安装包瘦身总结

前段时间APP要做资源压缩,需要把项目中使用的所有图片资源进行压缩,以减小APP安装包体积。想着既然压缩APP资源是为了缩小APP体积,那么来一遍APP整体瘦身流程并做一下总结吧。 整个过程分三步: 1.瘦身前分析 2.瘦身策略制定并实施 3.结果对比 瘦身前分析 安装包分析 iOS安装包有两种状态