点击上方蓝字 江湖评谈设为关注/星标
前言
.NET9里面进行了大量的优化,性能达到了前所未有的高度。可能大部分人只知道新的版本比旧的版本性能又提升了。但具体如何提升的,如何优化的呢?并不是太清楚,本篇描述下.NET9里面一个典型的优化:归纳变量(Induction Variable,简称IV)优化。
IV优化
既是归纳变量优化,我们首先得理解什么是归纳变量?举个例子,比如说有一个for循环,for循环里面被迭代的计数器即是归纳变量。如下示例的变量m是for循环的计数器,所以m被称之为归纳变量。
for(int m=0;m<10;m++)
{
//do something
}
好了,上面了解下概念。下面我们继续。
在JIT进行机器码生成的时候,.NET9 JIT对上面的归纳变量m进行了优化。如何优化的呢,能够让.NET9提升性能?带着这个问题,继续看。
一般来说我们开发的应用程序会运行在X86/X64指令集上(Risc-v/Arm这里先略过),X86是32位宽度(4个字节),对应的是32位寄存器操作。x64即64位宽度(8字节),对应的是64位寄存器操作。
.NET JIT x64在归纳变量优化前的处理中,如果归纳变量是四字节,则通常用32位寄存器来保存。JIT编译器将此归纳变量视为32位变量(也即四字节)。如归纳变量m可能会呈现以下两种初始模式:
1:直接把归纳变量m赋给32位寄存器eax
mov eax, m
2:初始化归纳变量m为零,且赋给32位寄存器eax
xor eax,eax
以下代码例子,如果在x64中运行,归纳变量m被编译器视为四字节进行操作。
int[] array = { 1, 2, 3, 4, 5 };
for (int m = 0; m < array.Length; m++)
{
sum += array[m];
}
以上代码在x64中运行,所以此时我们访问的数组array地址是8字节。又因归纳变量m是四字节,所以这里需要对归纳变量m进行零扩展到64位(同位宽),以便后面对数组进行取索引操作。
movzx rax, eax //将eax零扩展到64位寄存器rax
零扩展操作:顾名思义,把四字节扩展到八字节,多出来的字节部分用零填充。
零扩展操作,需要执行额外的JIT代码判断(判断归纳变量是四字节还是八字节)和生成(根据判断是否生成零扩展机器码),以及JIT运行机器码的时候需要额外的执行零扩展。循环基本上是代码里面常用的场景,在归纳变量未优化前,大部分时候JIT的这种操作可能会造成开销过大的情况。
很容易的看到这里影响性能的地方在JIT判断是否需要零扩展,JIT生成零扩展,以及JIT执行零扩展三个方面。
所以,优化点就在JIT这儿啦。即干掉零扩展这个麻烦可以极大提升性能,如何干掉呢?
NET9 JIT中对归纳变量一律定义为64位8字节,无论其是四字节还是八字节。
消除循环过程中冗余的零扩展指令集执行操作。这里的消除步骤和代码包括了JIT判断和JIT生成零扩展的代码,以及JIT执行零扩展机器码的操作。
那么最终的汇编优化结果如下:
优化前代码:
mov eax, m //将32位变量m加载到寄存器eax
movzx rax, eax //将eax零扩展到64位寄存器rax
//加载数组地址到 rcx
//通过扩展后的索引访问数组
优化后代码:
mov rax, m //将m加载到64位寄存器rax
mov rcx, [arr] //加载数组地址到rcx
mov rdx, [rcx+rax*4] //直接通过64位索引访问数组
优化后的代码去掉了零扩展操作,对于大量的循环操作,以上机器码的节省,会大大提高运行的性能。但是这里优化不止于此,同时也去掉了JIT对于零扩展的判断,零扩展的机器码生成两项。编译和运行的速度同时提升了,可谓双赢。
这里并没有展示JIT对于归纳变量是否需要零扩展的判断和零扩展机器码生成的部分。这部分非常复杂,代码量过多,如果喜欢看后台Call我,以后有时间聊聊。
结尾
以上就是.NET9 JIT里面一个典型的归纳变量优化,它的变动其实非常简单,也即是把归纳变量存储到64位寄存器里面去。这个存储操作就像一个撬动地球的杠杆,它一下子把JIT判断,生成,执行部分的代码全部进行了冗余消除。
虽然简单,但优化可能对大部分人来说依然不易。它的优化认知需要对于JIT认知和对于性能敏感。
以上就是本篇内容,欢迎点赞,关注,转发,收藏。
往期精彩回顾