.NET9 AOT的性能优化

摘要:NET9里面重要的一个优化是对于AOT预编译的内联优化,这种优化较高的提升了AOT运行的性能。本篇看下这种优化技术。

前言

.NET9里面重要的一个优化是对于AOT预编译的内联优化,这种优化较高的提升了AOT运行的性能。本篇看下这种优化技术。

优化从来都不是简单的去掉几行代码或者改动几个机器码就行了,需要统筹考虑,以AOT优化来参考说明。

.NET9里面AOT的优化主要聚焦于内联上面。内联优化虽然提高了AOT后程序运行的速度,但会膨胀二进制可执行文件的体积。这个体积太大,程序开发者肯定难以忍受。所以这中间需要做个平衡性的处理,也即是在体积能够膨胀适度的情况下,最大限度化的提升程序运行性能。

以防部分小伙伴不知道内联是什么,这里简单的说明下。当一个函数(设若函数ABC)频繁的调用另外一个函数(设若函数DEF),调用超过了一定的次数,编译器会发现这种调用热点(次数),会把DEF函数的代码放入到ABC函数里面去,让这两个函数形成一个函数ABC,且删除原有的DEF函数。

这里为了较明白的说清楚内联,部分编译器的操作如下

内联之前:

void DEF{ int x=0x10;}void ABC{ DEF;}

内联之后:

void ABC{ int x=0x10;}

很明显内联之后,编译器直接删除掉DEF函数,把DEF代码移动到了ABC函数里面。

这是一个非常简单的内联,因为少了一个函数,它非常明显的优化是避免帧寄存器(RBP)和栈寄存器(RSP)频繁的出入栈和保存导致的额外开销操作,用以提高性能。实际上的更复杂,举个例子比如在一些编译器中,发现DEF函数里面的int变量x并没有做任何事情,激进下的优化直接把变量x也给删除了。

回到正题,上面略微了解下优化的关键点。注意,本篇的AOT的内联优化是直接在编译阶段,无论是否有热点都会一次性的优化到可执行文件二进制的结果。我们下面继续看AOT的内联优化操作。

AOT内联的优化主要有以下几个方面,其一:值类型(只读结构体)的内联。其二:部分泛型的内联。其三:代码少且使用频繁的属性内联。

以下所有演示代码的机器码是AOT后的结果。

1.值类型内联

只读结构体的内联优化,下面代码:

readonly struct Test //readonly结构所有成员都是只读的 { public int X { get; } public int Y { get; } public Test(int x, int y) { X = x; Y = y; } public int JISuan => X * X + Y * Y; // 这里内联}static void Main{ Test test = new Test(1, 2); int jisuan = test.JISuan; // 直接内联计算}

AOT之后如下代码变成了一个机器指令,不仅进行了内联,且直接计算出了结果赋值给了jisuan这个变量。非常精简,性能自然不用多说,杠杠的。

代码:int jisuan = test.JISuan; 变成了如下机器码:优化后的ASM码:mov ecx,5

注意看,以上代码为什么不把只读结构及其字段,函数在for循环里的内联,只内联部分。如果全部内联ILC(AOT编译器)的压力确实小了,因为它不需要对代码进行判断,直接内联即可。这个问题,涉及到本篇开头提到的在性能和体积之间的平衡。只内联运行的部分,而无关紧要的则直接剔除,既保证了速度又保证了体积的适度。

2.部分泛型的内联

List泛型内联的操作

var list = new List;for (int i = 0; i { list.Add(i); // 对 List.Add 的调用在 AOT 下可内联}

for循环部分list.Add(i),常规的运行是先进行i变量自增,判断i变量是否小于10,如果是则调用list.Add函数把i变量放入到list里面去。而内联优化之后代码如下,可以看到是非常精炼的部分。完全摒弃了Add函数的调用,把Add函数的代码放入到for循环进行了内联。

loop: mov dword ptr [rdi+esi*4+10h],esi //把变量i放入到list inc esi //索引i进行了自增 cmp esi,0Ah //如果索引i小于10 jl loop //继续跳转到loop自增i变量加入到到list

另外一个例子就是Span泛型的的操作:

int array = { 1, 2, 3, 4, 5 }; var span = new Span(array); var slice = span.Slice(1, 3); // Span.Slice 内联优化

内联后的代码,通过xmm寄存器操作,取出数组前四个元素到xmm里,后面再进行了一次取元素,最后定位到array数组元素1所在的地址

lea rcx,[AOT_Private] //array数组地址 movups xmm0,xmmword ptr [rcx] //取array数组前四个元素到xmm寄存器,因xmm一次性最多16字节,所以下面还需要取一次array元素 movups xmmword ptr [rax+10h],xmm0 //把前四个元素放入到内存mov edx,dword ptr [rcx+10h] //取array数组的第五个元素mov dword ptr [rax+20h],edx //放到内存add rax,10h //array数组地址lea rbx,[rax+4] //取array数组的第一个元素的地址

以上简单来说就是进行了如下优化,直接内联了Slice函数,非常明显的看到了优化的凝练:

代码:var slice = span.Slice(1, 3);内联优化后的ASM代码:lea rbx,[rax+4] //rbx即指向span第一个元素地址

3,属性内联

class JiShu { private int _jishu; public int Jishu { get => _jishu; // 属性的 get 方法 set => _jishu = value; // 属性的 set 方法 } public void Add => _jishu++; // 调用 get 和 set}static void Main{ var jishu= new JiShu; for (int i = 0; i { jishu.Add; // get 和 set 方法在此被频繁调用 }}

因为频繁对属性进行操作,jishu.Add函数被for循环内联后的代码如下,摒弃了jishu.Add函数,把其代码放入到for循环进行了内联。循环计数,非常简洁

loop: mov eax,dword ptr [rsp+30h] //获取_jishu的值 inc eax //每循环一次自增1 mov dword ptr [rsp+30h],eax //把值赋给其地址 dec ecx //把索引自减1,注意索引是从5减到0为止 jne loop //如果索引不等于0,继续循环

AOT优化通过只读结构体,部分泛型,以及属性这三种语法的内联优化,进行了AOT性能的提升。优化之后的代码,凸显了可见性的精简和凝练。

这依然只是部分优化,可以预见后续的.NET10,11,12等等在AOT上有更大性能的提升。

来源:opendotnet

相关推荐