.Net8优化技术之常量优化

前言

常量经常是代码里面不可或缺的,常量与一些表达式组合或者自身是一个表达式需要一些计算结果。这些常量如果能被直接计算出来,则可以进行适度的优化。常量优化目前在.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()值直接相加。达到原有几倍以上的优化程度。