前言
常量经常是代码里面不可或缺的,常量与一些表达式组合或者自身是一个表达式需要一些计算结果。这些常量如果能被直接计算出来,则可以进行适度的优化。常量优化目前在.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 8
IL_0000: ldc.i4.s 23
IL_0002: ret
Method B
// 代码大小 9 (0x9)
.maxstack 8
IL_0000: ldarg.0
IL_0001: call instance int32 Program::A()
IL_0006: ldc.i4.2
IL_0007: mul
IL_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,17
ret
; Total bytes of code 6
```
## .NET 8.0.0 (8.0.23.17408), X64 RyuJIT AVX2
```assembly
; Program.B()
mov eax,2E
ret
; 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_000
private 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,2FAF080
ret
; 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,eax
mov [rdx],rax
mov [rdx+8],rax
mov word ptr [rdx+10],39
mov word ptr [rdx+12],1
mov rax,rdx
ret
; 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,1
mov ecx,39
xor r8d,r8d
mov [rdx],r8
mov [rdx+8],r8
mov [rdx+10],cx
mov [rdx+12],ax
mov rax,rdx
ret
; 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,28
call Program.SomethingElse()
add eax,10
add eax,10
add eax,10
add eax,10
add eax,10
add rsp,28
ret
; 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,28
call qword ptr [7FFEE5C11180]; Program.SomethingElse()
add eax,50
add rsp,28
ret
; Total bytes of code 18
看到.Net8里面几个Value被优化成了0x50,与返回的SomethingElse()值直接相加。达到原有几倍以上的优化程度。