前言
委托是一直被诟病的一个设计或者性能问题,好像太笨重了。这其实前端代码,也就是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, edi
8B549510 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, 0x7FF8923BEB88
48394E18 cmp qword ptr [rsi+18H], rcx
7520 jne SHORT G_M000_IG07
446BFA2A imul r15d, edx, 42
首先它会判断当前的委托函数的地址是这个委托已经被编译的函数地址相同,如果相同则直接调用内联的func委托的代码:i => i * 42,非常轻盈。如果不相同,说明没有被编译,跳到它在未优化版本中。这就是一个GDV实际应用的例子。通过一个if判断,是否相同。需要调用的是优化的版本还是非优化的版本。
5.看下它的基准
方法 运行时 平均值
禁用PGO:
DelegatePGO .NET 6.0 1.545 us
DelegatePGO .NET 7.0 1.532 us
启用PGO:
DelegatePGO .NET 6.0 1,427.7 ns
DelegatePGO .NET 7.0 436.0 ns
DelegatePGO .NET 8.0 431.0 ns
结尾
作者:江湖评谈
参考地址:
https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/