欢迎光临散文网 会员登陆 & 注册

编写高性能.net代码-JIT

2023-03-19 14:29 作者:79732017138_bili  | 我要投稿

     哈哈,本章就当了解吧,底层的内容需要自己去单独学习了


托管程序在运行时会加载CLR,CLR会先执行一些封装代码,都是汇编代码。程序集的托管代码在第一次运行时,都会执行一小段调用即时编译器(Just-in-Time,JIT)的“桩”代码(Stub),JIT会把方法的IL转换为硬件汇编指令

大部分时候,每个方法只需要经过一次JIT编译。但如果方法带有泛型(Generic Type)参数则不一定,这时有可能会对每一种不同类型的参数都调用一次JIT

如果你的应用程序或者用户很在意第一次JIT编译造成的延时,那么你就需要特别关注一下。大部分应用程序只是关心稳定运行期间的性能,但如果你需要很高的可用性,那么JIT可能会成为一个需要优化的问题

3.1 JIT编译的好处

引用的就近访问可能性很高——一起调用的代码常常会存放在同一个内存页中,避免了缺页中断的开销

内存占用降低——只会对真正用到的方法进行编译

交叉汇编内联化(Cross-assembly Inlining)——可以把其他DLL中的方法(包括 .NET Framework中的)内嵌到你的应用程序中,以显著提升性能

可以针对硬件特性进行优化,但在实践中针对特定平台的优化十分有限

.NET中的大部分代码优化都不是在语言的编译器中实现的(从C#/VB.NETIL),而是发生在JIT编译器的即时编译过程中

3.2 JIT编译的开销

JIT在幕后的处理过程

3.3 JIT编译器优化

JIT编译器会进行一些标准的代码优化工作,比如方法内联和去除数组范围检查代码

JIT最大的优化工作就是方法内联

就是把方法体内的代码嵌入调用的位置,避免原先的方法调用。对于那些会被频繁调用的小型函数而言,代码内联比较有意义,因为小型函数的调用开销会大于代码本身的执行开销

以下这些情况都会阻止方法内联的发生

虚方法

同一处调用了接口(Interface)的多种实现代码

循环

异常处理函数

递归

方法体的IL编码大于32个字节

3.4 减少JIT编译时间和程序启动时间

JIT编译器关乎性能的另一个主要因素是生成代码所耗费的时间,归根结底最主要还是取决于需要编译的代码量

特别注意

可能有大量代码是你看不到的,实际执行的代码明显要比源代码中看到的要多得多。对全部隐藏代码进行JIT编译可能需要耗费相当多的时间。特别是正则表达式和代码自动生成,有可能会生成大量重复的代码

LINQ

LINQ简洁的语法,把执行查询时实际运行的代码数量隐藏了起来。LINQ还把生成委托、分配内存等行为也都隐藏了。简单的LINQ查询也许没有问题,但最重要的一点是你应该进行确切的性能评估

dynamic关键字

dynamic关键字带来的主要问题,也是因为它会转换为大量的代码

正则表达式

正则表达式会转换为一个动态程序集中的IL状态机,然后进行JIT编译。这在一开始会多花一些时间,但重复执行时就能节省很多时间

代码自动生成

还有JIT之外的因素也会影响启动时间

比如I/O就会增加启动开销

PerfView 分析JIT代码桩

 

3.5 利用Profile优化JIT编译

Profile文件可被用于代码执行之前的生成过程。Profile的记录过程运行在独立的线程中,保存下来的Profile可以让生成的代码获得与JIT编译相同的就近访问可能性(Locality)。该Profile在程序每次执行时都会自动更新

启用Profile

ProfileOptimization.SetProfileRoot(@”C:\MyAppProfile”); ProfileOptimization.StartProfile(“default”);

3.6 使用NGEN的时机(简略)

NGEN把IL汇编代码转换为本机映像,实际上就是运行JIT编译器并把编译结果保存到本机映像程序集缓存目录中

NGEN一般是作为最后的手段来使用

第一个缺点就是丧失了对象引用的就近访问可能性

你可能还失去了某些优化效果,比如交叉汇编的内联化

如果应用程序的启动(“预热”)开销太高,上一节所述的Profile优化效果也无法满足性能需求,可能就该轮到NGEN上场了

决定使用NGEN之前,请牢记性能优化的基本指导原则是:评估、评估、再评估

3.6.2 本机代码生成

3.7 JIT无法胜任的场合

比如有一些处理器指令JIT是不会去使用的,即便当前的处理器能够支持也不会。数量最多的就是大部分SSE和SIMD指令

JIT编译器另一个不如本机代码编译器的地方,就是托管数组与直接访问本机内存的对比

直接访问本机内存通常意味着无需复制内存,而托管代码却是需要封送(Marshalling)的。虽然有办法绕过内存复制,比如用UnmanagedMemoryStream把本机缓冲区封装为Stream,但这样实际上你是在做不安全的内存访问

果应用程序要执行大量的数组或矩阵计算,你就不得不在性能和安全性之间做出平衡

如果你真的发现本机代码的效率要更高一些,可以尝试用P/Invoke把所有数据封送给本机代码编写的函数,把计算交给高度优化的C++ DLL完成,然后把结果返回给托管代码

3.8 评估

3.8.1 性能计数器

# of IL Bytes Jitted

# of Methods Jitted

Time in Jit

IL Bytes Jitted / sec

Standard Jit Failures

Total # of IL Bytes Jitted(和“# of IL Bytes Jitted”完全一样)

Time Loading

Bytes in Loader Heap

Total Assemblies

Total Classes Loaded

3.8.2 ETW事件

利用ETW事件,对每一个JIT编译过的方法,你都可以得到大量详细的性能数据,包括IL代码大小和JIT编译耗时

MethodID——该方法的唯一ID。

ModuleID——该方法所属模块(Module)的唯一ID。

MethodILSize——该方法IL代码的大小。

MethodNameSpace——与该方法关联的完整命名空间名称。

MethodName——方法名称。

MethodSignature——方法的签名,以逗号分隔的类型名称列表

MethodFlags:

◎0x1——动态方法。

◎0x2——泛型方法。

◎0x4——经JIT编译的方法(否则为NGEN处理过的方法)。

◎0x8——JIT助手方法。

3.8.3 找出JIT耗时最长的方法和模块

JIT的耗时通常与方法中的IL指令数量直接相关,但由于类型加载时间也可能包含在JIT耗时中,问题就变得复杂了,特别是当模块被第一次用到时

3.9 小结

请认真考虑那些有可能大量自动生成的代码

Profile优化可以对绝大部分使用到的代码进行并行JIT编译,可用于减少应用程序的启动时间

如果要从函数内联中获得性能收益,请避免用到虚方法、循环、异常处理、递归和代码较多的方法体

对于大型应用,或者对程序启动阶段的JIT开销无法容忍,可以考虑使用NGEN,在使用NGEN之前,请用MPGO对本机映像进行优化


本文使用 文章同步助手 同步


编写高性能.net代码-JIT的评论 (共 条)

分享到微博请遵守国家法律