前言
常量经常是代码里面不可或缺的,常量与一些表达式组合或者自身是一个表达式需要一些计算结果。这些常量如果能被直接计算出来,则可以进行适度的优化。常量优化目前在.Net8里面有Roslyn前端优化,以及JIT后端优化两种。本篇,看下这些优化的方式。.
概括
1.常量表达式的优化
C# Source
[Benchmark]public int A() => 3 + (4 * 5);[Benchmark]public int B() => A() * 2;
Roslyn IL
Method A()// 代码大小 3 (0x3).maxstack 8IL_0000: ldc.i4.s 23IL_0002: retMethod B// 代码大小 9 (0x9).maxstack 8IL_0000: ldarg.0IL_0001: call instance int32 Program::A()IL_0006: ldc.i4.2IL_0007: mulIL_0008: ret
在函数A里面可以看到在前端Roslyn里面它就会计算出常量表达式:3 + (4 * 5)的值。
JIT Machine Cdoe
## .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2```assembly; Program.A()mov eax,17ret; Total bytes of code 6```## .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2```assembly; Program.B()mov eax,2Eret; Total bytes of code 6```
JIT直接返回结果,并没有与函数A()计算结果。可以看到优化的很完美。
2.内联常量优化
C# Code
public static TimeSpan FromSeconds(double value) => Interval(value, TicksPerSecond); // TicksPerSecond is a constant ==10_000_000private static TimeSpan Interval(double value, double scale) => IntervalFromDoubleTicks(value * scale);private static TimeSpan IntervalFromDoubleTicks(double ticks) => ticks == long.MaxValue ? TimeSpan.MaxValue : new TimeSpan((long)ticks);
把这段代码内联起来,变成了如下所示:
public static TimeSpan FromSeconds(double value){double ticks = value * 10_000_000;return ticks == long.MaxValue ? TimeSpan.MaxValue : new TimeSpan((long)ticks);}
JIT Machine Code
## .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2```assembly; Program.FromSeconds()mov eax,2FAF080ret; Total bytes of code 6```
它只是返回一个常量,非常不错。没有复杂的分支和复杂的判断计算。以及new的可能巨大的开销。
3.结构体的赋值常量优化
C# Code
[Benchmark]public Color DarkOrange() => Color.DarkOrange;
.Net 8 JIT Machine Code
## .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2```assembly; Program.DarkOrange()xor eax,eaxmov [rdx],raxmov [rdx+8],raxmov word ptr [rdx+10],39mov word ptr [rdx+12],1mov rax,rdxret; Total bytes of code 25```
39和1这两个常量被返回Color结构体相应的位置。但是在.Net6里面,它多做了一层寄存器传递。
.Net 6 JIT Machine Code
## .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2```assembly; Program.DarkOrange()mov eax,1mov ecx,39xor r8d,r8dmov [rdx],r8mov [rdx+8],r8mov [rdx+10],cxmov [rdx+12],axmov rax,rdxret; Total bytes of code 32```
先把常量39和1给了寄存器exc,eax。然后在把这两个寄存器赋值给Color结构体相应的内存。这个过程看起来是可以优化的,就是上面的.Net JIT 的Machine Code优化代码。根据结构体的内存来优化掉冗余代码,直接把结果赋值给结构体内存位置。
4.函数与多个常量表达式操作的优化
C# Code
[Benchmark]public int Compute1() => Value + Value + Value + Value + Value;[Benchmark]public int Compute2() => SomethingElse() + Value + Value + Value + Value + Value;private static int Value => 16;[MethodImpl(MethodImplOptions.NoInlining)]private static int SomethingElse() => 42;
无论在.Net6或者.Net8函数Compute1都是被优化成常量0x50直接返回,关键点在于Compute2函数的优化。
.Net6 JIT Machine Code for Compute2
## .NET 6.0.8 (6.0.822.36306), X64 RyuJIT AVX2```assembly; Program.Compute2()sub rsp,28call Program.SomethingElse()add eax,10add eax,10add eax,10add eax,10add eax,10add rsp,28ret; Total bytes of code 29```
.Net6里面几个Value常量通过寄存器eax相加,这种看似并不是太美观,而且性能上也有问题。
.Net8 JIT Machine Code for compute2
## .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2```assembly; Program.Compute2()sub rsp,28call qword ptr [7FFEE5C11180]; Program.SomethingElse()add eax,50add rsp,28ret; Total bytes of code 18
看到.Net8里面几个Value被优化成了0x50,与返回的SomethingElse()值直接相加。达到原有几倍以上的优化程度。