.Net8 PGO的GDV是如何提升委托(delegate)性能的呢

前言

委托是一直被诟病的一个设计或者性能问题,好像太笨重了。这其实前端代码,也就是C#源码和IL代码之类的设计生成上比之其它C#代码的优雅,显得有的格格不入和丑所导致的(人们的第一感受),后端的JIT也是在优化方面并不够所导致的。.Net7 PGD里面的GDV(虚拟保护)对JIT进行了优化,可以让委托牺牲一个分支的判断,达到直接调用函数性能的效果。以下通过.Net8 PreView3观察下这种效果。.

概括

1.官方C#代码

static int[] s_values = Enumerable.Range(0, 1_000).ToArray();static void Main(){   for (int i = 0; i < 1_000_000; i++)   Sum(s_values, i => i * 42);}[MethodImpl(MethodImplOptions.NoInlining)]  static int Sum(int[] values, Func<int, int> func){   int sum = 0;   foreach (int value in values)     sum += func(value);   return sum;}

2.代码分析

这段代码,是通过for循环Sum函数,Sun函数里面又foreach循环了委托func变量。这个func就是本节的重点了。
注意看在Sum函数的第二个参数里面有一个委托的表达式:i = i * 42。

3.未启用PGO
看下未启用PGO调用func的第0层代码:

488B4908             mov      rcx, gword ptr [rcx+08H]8B55B0               mov      edx, dword ptr [rbp-50H]488B4518             mov      rax, gword ptr [rbp+18H]FF5018               call     [rax+18H]System.Func`2[int,int]:Invoke(int):int:this

按部就班的生成机器码,基本上没有优化。

看下未启用PGO调用func的第一层的代码:

8BD7                 mov      edx, edi8B549510             mov      edx, dword ptr [rbp+4*rdx+10H]488B4E08             mov      rcx, gword ptr [rsi+08H]FF5618               call     [rsi+18H]System.Func`2[int,int]:Invoke(int):int:this

基本上没有优化

4.启用PGO
0层依然未有明显的优化

mov      rcx, gword ptr [rcx+08H]8B55B0               mov      edx, dword ptr [rbp-50H]488B4598             mov      rax, gword ptr [rbp-68H]FF5018               call     [rax+18H]System.Func`2[int,int]:Invoke(int):int:this

1层有非常明显的优化

48B988EB3B92F87F0000 mov      rcx, 0x7FF8923BEB8848394E18             cmp      qword ptr [rsi+18H], rcx7520                 jne      SHORT G_M000_IG07446BFA2A             imul     r15d, edx, 42

首先它会判断当前的委托函数的地址是这个委托已经被编译的函数地址相同,如果相同则直接调用内联的func委托的代码:i => i * 42,非常轻盈。如果不相同,说明没有被编译,跳到它在未优化版本中。这就是一个GDV实际应用的例子。通过一个if判断,是否相同。需要调用的是优化的版本还是非优化的版本。

5.看下它的基准

方法 运行时 平均值禁用PGO:DelegatePGO .NET 6.0 1.545 usDelegatePGO .NET 7.0 1.532 us启用PGO:DelegatePGO .NET 6.0 1,427.7 nsDelegatePGO .NET 7.0 436.0 nsDelegatePGO .NET 8.0 431.0 ns