前几天笔者提交了关于FasterKvCache的性能优化代码,其中有一个点就是我把一些后续不需要继承的类设置为了sealed密封类,然后就有小伙伴在问,为啥这个地方需要设置成sealed? 提交的代码如下所示: 一般业务开发的同学可能接触密封类比较少,密封类除了框架设计约束(不能被继承)以外,还有一个微小的性能提升,不过虽然它是一个微小的优化点,多框架开发的作者都会做这样的优化,如果方法调用的频次很高,那也会带来很大的收益。 笔者最开始是从。NETruntime中的代码学习到这一个优化技巧,后面有看到meziantou大佬的文章performancebenefitsofsealedclass〔1〕完整的学习了一下。 然后本来是想翻译一下这篇文章,找了下发现Weihan大佬今年年初翻译了meziantou大佬的文章,质量非常高的中文版,大家可以戳链接看看,既然如此在本文中带大家回顾一下文章中例子,另外从JITASM的层面分析为什么性能会有提升。性能优势虚方法调用 在上面提到的文章例子中,有一个虚方法的调用,大家其实要明白一点,现在面向对象的封装、继承、多态中的多态实现主要就是靠虚方法。 一个类型可能会有子类,子类可能会重写类型的方法从而达到不同的行为(多态),而这些重写的方法都在虚方法表里,调用的话就需要查表。 回到文中的代码,大佬构建了一个这样的测试用例: publicclassSealedBenchmark { readonlyNonSealedTypenonSealedTypenew(); readonlySealedTypesealedTypenew(); 〔Benchmark(Baselinetrue)〕 publicvoidNonSealed() { JIT不能知道nonSealedType的实际类型。 它可能已经被另一个方法设置为派生类。 所以,为了安全起见,它必须使用一个虚拟调用。 nonSealedType。Method(); } 〔Benchmark〕 publicvoidSealed() { JIT确信sealedType是一个SealedType。由于该类是密封的。 它不可能是一个派生类型的实例。 所以它可以使用直接调用,这样会更快。 sealedType。Method(); } } 基类 internalclassBaseType { publicvirtualvoidMethod(){} } 非密封的派生类 internalclassNonSealedType:BaseType { publicoverridevoidMethod(){} } 密封的派生类 internalsealedclassSealedType:BaseType { publicoverridevoidMethod(){} } 取得的结果就是密封类要比非密封的快98。 那么为什么会这样呢?首先我们来比较一下两个方法的IL代码,发现是一模一样的,对于方法调用都是用了callvirt(它就是用来调用虚方法的,想了解更多详情可以看这里〔2〕),因为instance是从字段中加载的,编译器无法知道具体的类型,只能使用callvirt。 那区别在哪里呢?我们可以看到JIT生成后的汇编代码,可以很清楚的看到密封类少了两条指令,因为JIT可以从密封类中知道它不可能被继承,也不可能被重写,所以是直接跳转到密封类目标方法执行,而非密封类还有一个查表的过程。而现在很多大佬聊天说JIT的去虚拟化其实主要就是在JIT编译时去除了callvirt调用。 另外文中也提到了一段代码,如果JIT能确定类型,也是直接调用的: voidNonSealed() { varinstancenewNonSealedType(); instance。Method();JIT知道instance是NonSealedType,因为它是在方法中被创建的, 从未被修改过,所以它使用直接调用 } voidSealed() { varinstancenewSealedType(); instance。Method();JIT知道类型是SealedType,所以直接调用 } 此时两者的汇编代码没有任何区别,都是直接jmp到目标方法。 发现一个有趣的东西,如果我们切到。NETFramework的JIT,可以发现。NETFramework的JIT没有。NET生成的这么高效,没有直接jmp到目标方法,而是多了一层call和ret。所以,朋友们还等什么呢?快升级。NET版本吧。对象类型转换(isas) 同样有下面这样一段代码,测试密封类和非密封类的对象类型转换性能: publicclassSealedBenchmark { readonlyBaseTypebaseTypenew(); 〔Benchmark(Baselinetrue)〕 publicboolIsSealed()baseTypeisSealedType; 〔Benchmark〕 publicboolIsNonSealed()baseTypeisNonSealedType; } internalclassBaseType{} internalclassNonSealedType:BaseType{} internalsealedclassSealedType:BaseType{} 毫无疑问,密封类快91。 IL层面,两个方法都是一模一样: 可以看到密封类的代码相当高效,直接比较一下就转换类型返回了,而非密封类还需要call方法走查表流程:数组 。NET的数组是协变的,协变兼容的话就意味着在添加进入数组时需要检查它的类型,而如果是密封类那就可以删除检查,同样有下面一段代码: publicclassSealedBenchmark { SealedType〔〕sealedTypeArraynewSealedType〔100〕; NonSealedType〔〕nonSealedTypeArraynewNonSealedType〔100〕; 〔Benchmark(Baselinetrue)〕 publicvoidNonSealed() { nonSealedTypeArray〔0〕newNonSealedType(); } 〔Benchmark〕 publicvoidSealed() { sealedTypeArray〔0〕newSealedType(); } } internalclassBaseType{} internalclassNonSealedType:BaseType{} internalsealedclassSealedType:BaseType{} 密封类的性能要高14左右。 打开IL代码,两者编译出的方法都是一样的,但是跳转到汇编代码可以发现差别,同样的是Stelem。Ref给数组赋值,密封类只是检查了一下数组长度,然后直接赋值,而非密封类还需要调用System。Runtime。CompilerServices。CastHelpers。StelemRef进行检查才能完成赋值。将数组转换为Span 和数组一样,将数组转换为Span时也需要插入类型检查,有如下测试代码: publicclassSealedBenchmark { SealedType〔〕sealedTypeArraynewSealedType〔100〕; NonSealedType〔〕nonSealedTypeArraynewNonSealedType〔100〕; 〔Benchmark(Baselinetrue)〕 publicSpanNonSealed()nonSealedTypeArray; 〔Benchmark〕 publicSpanSealed()sealedTypeArray; } publicclassBaseType{} publicclassNonSealedType:BaseType{} publicsealedclassSealedType:BaseType{} 密封类的性能要高50: 同样,这也是IL一模一样的,在JIT阶段做的优化,可以明显的看到,JIT为非密封类单独做了类型检查:总结 笔者在FasterKvCache代码中将一些类设置为sealed的原因显而易见:为了让类的职责更加清晰,在设计中没有计划让它有派生类为了性能的提升,JIT优化可以让其方法调用更快 还有更多有趣的东西(比如IDE智能提示将类设置为密封,如何使用dotnetformat集成这些分析),大家可以翻阅原文或者Weihan大佬翻译的文章。https:www。meziantou。netperformancebenefitsofsealedclass。htmhttps:mp。weixin。qq。comsdZlEjOB8jx0ku8eN8AhpzQ。NET性能优化交流群 相信大家在开发中经常会遇到一些性能问题,苦于没有有效的工具去发现性能瓶颈,或者是发现瓶颈以后不知道该如何优化。于是很高兴的在这里宣布,我创建了一个专门交流。NET性能优化经验的群组,主题包括但不限于:如何找到。NET性能瓶颈,如使用APM、dotnettools等工具。NET框架底层原理的实现,如垃圾回收器、JIT等等如何编写高性能的。NET代码,哪些地方存在性能陷阱 希望能有更多志同道合朋友加入,分享一些工作中遇到的。NET性能问题和宝贵的性能分析优化经验。由于已经达到200人,可以加我微信,我拉你进群:ls1075 参考资料 〔1〕 performancebenefitsofsealedclass:https:www。meziantou。netperformancebenefitsofsealedclass。htm 〔2〕 这里:https:learn。microsoft。comzhcndotnetapisystem。reflection。emit。opcodes。callvirt?viewnet7。0