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

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

提交的代码如下所示:5710abcc7d9a67d8fa4ece3075b7ddbe.png

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

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

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

性能优势

虚方法调用

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

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

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

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%。7f08c1139a4ebcb96f99564ebf9946af.png

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

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

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

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

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

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

对象类型转换 (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%。9bb3e251766872d1df892bddd54542d0.png

IL 层面,两个方法都是一模一样:318abc731e0ef6178c472f80c73484e3.png

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

数组

.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%左右。197904156758006f57c111e0c1a7bed5.png

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

将数组转换为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%:77029232c132f41da02291de4d4fabfe.png

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

总结

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

  • 为了让类的职责更加清晰,在设计中没有计划让它有派生类

  • 为了性能的提升,JIT 优化可以让其方法调用更快

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

  • https://www.meziantou.net/performance-benefits-of-sealed-class.htm

  • https://mp.weixin.qq.com/s/dZlEjOB8jx0ku8eN8AhpzQ

.NET性能优化交流群

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

  • 如何找到.NET性能瓶颈,如使用APM、dotnet tools等工具

  • .NET框架底层原理的实现,如垃圾回收器、JIT等等

  • 如何编写高性能的.NET代码,哪些地方存在性能陷阱

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

c3e57ff0944e043d503cbc53ae18fb08.jpeg

参考资料

[1]

performance-benefits-of-sealed-class: https://www.meziantou.net/performance-benefits-of-sealed-class.htm

[2]

这里: https://learn.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.opcodes.callvirt?view=net-7.0

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/280822.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

powershell 常用命令笔记

常用集合&#xff0c;方便后续复制粘贴 # 判断文件在不在 # 输出文件 IF(!(test-path $filePath)) {$result|Out-File $filePath }# 读取txt $result(Get-Content $filePath -TotalCount 1).Trim() $result# 删除文件 remove-item "C:\wistron\Datasource\spiderPort.txt…

Linux 性能监控 : CPU 、Memory 、 IO 、Network

一、CPU 1.良好状态指标 CPU利用率&#xff1a;User Time < 70%&#xff0c;System Time < 35%&#xff0c;User Time System Time < 70% 上下文切换&#xff1a;与CPU利用率相关联&#xff0c;如果CPU利用率状态良好&#xff0c;大量的上下文切换也是可以接受的 可…

Java 打飞机(小游戏)[版权非本人 本人制作收藏整理]

今天在网络上 看到一个纯java的小游戏 代码copy到 myeclipse中 居然效果还不错 这是一些效果图 当然了 图片是我自己找的 有心兴趣的朋友可以做的好看一点 具体的代码 都放在自己的文件里去了 那么可以去下载 https://i.cnblogs.com/Files.aspx 转载于:https://www.cnblogs…

Cygwin使用指南

1 引言cygwin是一个在windows平台上运行的unix模拟环境&#xff0c;是cygnus solutions公司开发的自由软件&#xff08;该公司开发了很多好东西&#xff0c;著名的还有eCos&#xff0c;不过现已被Redhat收购&#xff09;。它对于学习unix/linux操作环境&#xff0c;或者从unix到…

nest 架构_当有人打来您的Nest Hello时,如何让Google Home通知您

nest 架构The Nest Hello can alert you on your phone whenever someone rings your doorbell, but if you have a Google Home, you can also have Google Assistant audibly announce that someone is at the door. 无论何时有人按下门铃&#xff0c; Nest Hello都会在电话上…

如何序列化派生类

前言假设有一个 Person 抽象基类&#xff0c;其中包含 Student 和 Teacher 派生类&#xff1a;public class Person {public string Name { get; set; } }public class Student : Person {public int Score { get; set; } }public class Teacher : Person {public string Title…

OPC Client “failed to execute OPCENUM” 解决方法

进入cmd重新执行 OpcEnum.exe /regserver 即可。

django07: 模板语言(旧笔记)

详见&#xff1a;https://www.cnblogs.com/liwenzhou/p/7931828.html#autoid-2-3-6 包含&#xff1a; 模板 块 组件 静态文件

block,inline和inline-block概念和区别

block&#xff1a;block-level elements (块级元素) &#xff0c;inline&#xff1a; inline elements (内联元素)。block元素通常被现实为独立的一块&#xff0c;会单独换一行&#xff1b;inline元素则前后不会产生换行&#xff0c;一系列inline元素都在一行内显示&#xff0c…

Hadoop3.0 WordCount测试一直Accept 状态,Nodes of the cluster 页面node列表个数为0

起因是我运行wordcount测试一直卡主&#xff0c;不能执行&#xff0c;一直处于 Accept 状态&#xff0c;等待被执行&#xff0c;刚开始是各种配置yarn参数&#xff0c;以及host配置&#xff0c;后来发现还是不行 hadoop 集群安装完成后&#xff0c;在50070的 HDFS 管理后台能看…

nexus 手动增加_如何使用Google的工厂图像手动升级Nexus设备

nexus 手动增加Google’s Nexus devices are supposed to receive timely updates, but the staggered rollout means it can take weeks for devices to receive over-the-air (OTA) updates. Luckily, there’s a faster (and geekier) way to install the latest version of…

教你创建Google网站地图Sitemap.xml(转)

http://teachmyself.blog.163.com/blog/static/18881422920119895248288/ Sitemap.xml是 google搞出来的&#xff0c;也就是网站地图&#xff0c;不过这个网站地图是用xml写的&#xff0c;而且要按google的标准来写&#xff0c;并且要将写出来的这个文件 sitemap.xml上传到自己…

Oracle存储过程语法

创建基本的存储过程 1 CREATE OR REPLACE PROCEDURE MyProName IS 2 BEGIN 3 NULL; 4 END; 行1:CREATE OR REPLACE PROCEDURE 是一个SQL语句通知Oracle数据库去创建一个叫做skeleton存储过程, 如果存在就覆盖它; 行2:IS关键词表明后面将跟随一个PL/SQL体。 行3:BEGIN关键词表…

WPF-16 图形处理

我们这节主要介绍WPF常用画图标签&#xff0c;由于WPF图形处理设计大量篇幅 ,我们在这里抛砖引玉&#xff0c;具体更多的学习资料链接https://github.com/microsoft/WPF-Samples/tree/master/Graphics 该链接中微软提供了大量的学习Demo&#xff0c;WPF图形处理最大的区别在于…

powershell 文件/文件夹操作

新建文件夹 New-Item -ItemType Directory -Force -Path $TargetPath复制文件夹到另外文件夹 Copy-Item <源文件夹> <新文件夹> -recurse -force 复制文件&#xff08;与修改文件名&#xff09; // 达到复制文件到新文件夹&#xff0c;及修改文件名效果 copy-…

纯CSS制作各种各样的网页图标(三角形、暂停按钮、下载箭头、加号等)

三角形 <div class"box"></div> <style>.box{ width: 0;height: 0;border-top: 50px solid transparent;border-bottom: 50px solid transparent;border-left: 50px solid transparent;border-right: 50px solid red; } </style> 平行四边形…

您的MyFitnessPal帐户几乎肯定已被黑客入侵,请立即更改密码

If you’re one of the millions of the 150 million MyFitnessPal users, bad news: hackers have your email address, your user name, and your hashed password. 如果您是1.5亿MyFitnessPal用户中的数百万用户之一&#xff0c;那么这是个坏消息&#xff1a;黑客拥有您的电…

Oracle Grid 11.2.0.4 安装是出现INS-30510: Insufficient number of ASM disks selected.

最新文章&#xff1a;Virsons Blog 错误的原因是由于磁盘数和冗余层级不匹配&#xff1a; 如果创建用来存放OCR和VOTEDISK的ASM磁盘组&#xff0c;那么External、Normal、High三种冗余级别对应的Failgroup个数是1、3、5。也就是说&#xff0c;创建这三种冗余级别的磁盘组至少分…

动态编译库 Natasha 5.0 版本发布

动态编译库 Natasha 5.0 于十月份发布&#xff0c;此次大版本更新带来了强大的兼容性支持&#xff0c;目前 Natasha 已支持 .NET Standard 2.0 及 .NET Core 3.1 以上版本&#xff08;包括 .NET Framework&#xff09;了。引入项目NuGet\Install-Package DotNetCore.Natasha.CS…

著名软件公司的java笔试算法题!(含参考答案)

原题如下&#xff1a;用1、2、2、3、4、5这六个数字&#xff0c;用java写一个main函数&#xff0c;打印出所有不同的排列&#xff0c;如&#xff1a;512234、412345等&#xff0c;要求&#xff1a;"4"不能在第三位&#xff0c;"3"与"5"不能相连.…