【译】.NET 7 中的性能改进(三)

原文 | Stephen Toub

翻译 | 郑子铭

PGO

我在我的 .NET 6 性能改进一文中写了关于配置文件引导优化 (profile-guided optimization) (PGO) 的文章,但我将在此处再次介绍它,因为它已经看到了 .NET 7 的大量改进。

PGO 已经存在了很长时间,有多种语言和编译器。基本思想是你编译你的应用程序,要求编译器将检测注入应用程序以跟踪各种有趣的信息。然后你让你的应用程序通过它的步伐,运行各种常见的场景,使该仪器“描述”应用程序执行时发生的事情,然后保存结果。然后重新编译应用程序,将这些检测结果反馈给编译器,并允许它根据预期的使用方式优化应用程序。这种 PGO 方法被称为“静态 PGO”,因为所有信息都是在实际部署之前收集的,这是 .NET 多年来一直以各种形式进行的事情。不过,从我的角度来看,.NET 中真正有趣的开发是“动态 PGO”,它是在 .NET 6 中引入的,但默认情况下是关闭的。

动态 PGO 利用分层编译。我注意到 JIT 检测第 0 层代码以跟踪方法被调用的次数,或者在循环的情况下,循环执行了多少次。它也可以将它用于其他事情。例如,它可以准确跟踪哪些具体类型被用作接口分派的目标,然后在第 1 层专门化代码以期望最常见的类型(这称为“保护去虚拟化 (guarded devirtualization)”或 GDV)。你可以在这个小例子中看到这一点。将 DOTNET_TieredPGO 环境变量设置为 1,然后在 .NET 7 上运行:

class Program {     static void Main()     {         IPrinter printer = new Printer();         for (int i = 0; ; i++)         {             DoWork(printer, i);         }     }      static void DoWork(IPrinter printer, int i)     {         printer.PrintIfTrue(i == int.MaxValue);     }      interface IPrinter     {         void PrintIfTrue(bool condition);     }      class Printer : IPrinter     {         public void PrintIfTrue(bool condition)         {             if (condition) Console.WriteLine("Print!");         }     } } 

DoWork 的第 0 层代码最终看起来像这样:

G_M000_IG01:                ;; offset=0000H        55                   push     rbp        4883EC30             sub      rsp, 48        488D6C2430           lea      rbp, [rsp+30H]        33C0                 xor      eax, eax        488945F8             mov      qword ptr [rbp-08H], rax        488945F0             mov      qword ptr [rbp-10H], rax        48894D10             mov      gword ptr [rbp+10H], rcx        895518               mov      dword ptr [rbp+18H], edx  G_M000_IG02:                ;; offset=001BH        FF059F220F00         inc      dword ptr [(reloc 0x7ffc3f1b2ea0)]        488B4D10             mov      rcx, gword ptr [rbp+10H]        48894DF8             mov      gword ptr [rbp-08H], rcx        488B4DF8             mov      rcx, gword ptr [rbp-08H]        48BAA82E1B3FFC7F0000 mov      rdx, 0x7FFC3F1B2EA8        E8B47EC55F           call     CORINFO_HELP_CLASSPROFILE32        488B4DF8             mov      rcx, gword ptr [rbp-08H]        48894DF0             mov      gword ptr [rbp-10H], rcx        488B4DF0             mov      rcx, gword ptr [rbp-10H]        33D2                 xor      edx, edx        817D18FFFFFF7F       cmp      dword ptr [rbp+18H], 0x7FFFFFFF        0F94C2               sete     dl        49BB0800F13EFC7F0000 mov      r11, 0x7FFC3EF10008        41FF13               call     [r11]IPrinter:PrintIfTrue(bool):this        90                   nop  G_M000_IG03:                ;; offset=0062H        4883C430             add      rsp, 48        5D                   pop      rbp        C3                   ret 

而最值得注意的是,你可以看到调用[r11]IPrinter:PrintIfTrue(bool):这个做接口调度。但是,再看一下为第一层生成的代码。我们仍然看到调用[r11]IPrinter:PrintIfTrue(bool):this,但我们也看到了这个。

G_M000_IG02:                ;; offset=0020H        48B9982D1B3FFC7F0000 mov      rcx, 0x7FFC3F1B2D98        48390F               cmp      qword ptr [rdi], rcx        7521                 jne      SHORT G_M000_IG05        81FEFFFFFF7F         cmp      esi, 0x7FFFFFFF        7404                 je       SHORT G_M000_IG04  G_M000_IG03:                ;; offset=0037H        FFC6                 inc      esi        EBE5                 jmp      SHORT G_M000_IG02  G_M000_IG04:                ;; offset=003BH        48B9D820801A24020000 mov      rcx, 0x2241A8020D8        488B09               mov      rcx, gword ptr [rcx]        FF1572CD0D00         call     [Console:WriteLine(String)]        EBE7                 jmp      SHORT G_M000_IG03 

第一块是检查IPrinter的具体类型(存储在rdi中)并与Printer的已知类型(0x7FFC3F1B2D98)进行比较。如果它们不一样,它就跳到它在未优化版本中做的同样的接口调度。但如果它们相同,它就会直接跳到Printer.PrintIfTrue的内联版本(你可以看到这个方法中对Console:WriteLine的调用)。因此,普通情况(本例中唯一的情况)是超级有效的,代价是一个单一的比较和分支。

这一切都存在于.NET 6中,那么为什么我们现在要谈论它?有几件事得到了改善。首先,由于dotnet/runtime#61453这样的改进,PGO现在可以与OSR一起工作。这是一个大问题,因为这意味着做这种接口调度的热的长期运行的方法(这相当普遍)可以得到这些类型的去虚拟化/精简优化。第二,虽然PGO目前不是默认启用的,但我们已经让它更容易打开了。在dotnet/runtime#71438dotnet/sdk#26350之间,现在可以简单地将true放入你的.csproj中。 csproj,它的效果和你在每次调用应用程序之前设置DOTNET_TieredPGO=1一样,启用动态PGO(注意,它不会禁止使用R2R图像,所以如果你希望整个核心库也采用动态PGO,你还需要设置DOTNET_ReadyToRun=0)。然而,第三,是动态PGO已经学会了如何检测和优化额外的东西。

PGO已经知道如何对虚拟调度进行检测。现在在.NET 7中,在很大程度上要感谢dotnet/runtime#68703,它也可以为委托做这件事(至少是对实例方法的委托)。考虑一下这个简单的控制台应用程序。

using System.Runtime.CompilerServices;  class Program {     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;     } } 

在没有启用PGO的情况下,我得到的优化汇编是这样的。

; Assembly listing for method Program:Sum(ref,Func`2):int ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; rsp based frame ; partially interruptible ; No PGO data  G_M000_IG01:                ;; offset=0000H        4156                 push     r14        57                   push     rdi        56                   push     rsi        55                   push     rbp        53                   push     rbx        4883EC20             sub      rsp, 32        488BF2               mov      rsi, rdx  G_M000_IG02:                ;; offset=000DH        33FF                 xor      edi, edi        488BD9               mov      rbx, rcx        33ED                 xor      ebp, ebp        448B7308             mov      r14d, dword ptr [rbx+08H]        4585F6               test     r14d, r14d        7E16                 jle      SHORT G_M000_IG04  G_M000_IG03:                ;; offset=001DH        8BD5                 mov      edx, ebp        8B549310             mov      edx, dword ptr [rbx+4*rdx+10H]        488B4E08             mov      rcx, gword ptr [rsi+08H]        FF5618               call     [rsi+18H]Func`2:Invoke(int):int:this        03F8                 add      edi, eax        FFC5                 inc      ebp        443BF5               cmp      r14d, ebp        7FEA                 jg       SHORT G_M000_IG03  G_M000_IG04:                ;; offset=0033H        8BC7                 mov      eax, edi  G_M000_IG05:                ;; offset=0035H        4883C420             add      rsp, 32        5B                   pop      rbx        5D                   pop      rbp        5E                   pop      rsi        5F                   pop      rdi        415E                 pop      r14        C3                   ret  ; Total bytes of code 64 

注意其中调用[rsi+18H]Func`2:Invoke(int):int:this来调用委托。现在启用了PGO。

; Assembly listing for method Program:Sum(ref,Func`2):int ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; optimized using profile data ; rsp based frame ; fully interruptible ; with Dynamic PGO: edge weights are valid, and fgCalledCount is 5628 ; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data  G_M000_IG01:                ;; offset=0000H        4157                 push     r15        4156                 push     r14        57                   push     rdi        56                   push     rsi        55                   push     rbp        53                   push     rbx        4883EC28             sub      rsp, 40        488BF2               mov      rsi, rdx  G_M000_IG02:                ;; offset=000FH        33FF                 xor      edi, edi        488BD9               mov      rbx, rcx        33ED                 xor      ebp, ebp        448B7308             mov      r14d, dword ptr [rbx+08H]        4585F6               test     r14d, r14d        7E27                 jle      SHORT G_M000_IG05  G_M000_IG03:                ;; offset=001FH        8BC5                 mov      eax, ebp        8B548310             mov      edx, dword ptr [rbx+4*rax+10H]        4C8B4618             mov      r8, qword ptr [rsi+18H]        48B8A0C2CF3CFC7F0000 mov      rax, 0x7FFC3CCFC2A0        4C3BC0               cmp      r8, rax        751D                 jne      SHORT G_M000_IG07        446BFA2A             imul     r15d, edx, 42  G_M000_IG04:                ;; offset=003CH        4103FF               add      edi, r15d        FFC5                 inc      ebp        443BF5               cmp      r14d, ebp        7FD9                 jg       SHORT G_M000_IG03  G_M000_IG05:                ;; offset=0046H        8BC7                 mov      eax, edi  G_M000_IG06:                ;; offset=0048H        4883C428             add      rsp, 40        5B                   pop      rbx        5D                   pop      rbp        5E                   pop      rsi        5F                   pop      rdi        415E                 pop      r14        415F                 pop      r15        C3                   ret  G_M000_IG07:                ;; offset=0055H        488B4E08             mov      rcx, gword ptr [rsi+08H]        41FFD0               call     r8        448BF8               mov      r15d, eax        EBDB                 jmp      SHORT G_M000_IG04 

我选择了i => i * 42中的42常数,以使其在汇编中容易看到,果然,它就在那里。

G_M000_IG03:                ;; offset=001FH        8BC5                 mov      eax, ebp        8B548310             mov      edx, dword ptr [rbx+4*rax+10H]        4C8B4618             mov      r8, qword ptr [rsi+18H]        48B8A0C2CF3CFC7F0000 mov      rax, 0x7FFC3CCFC2A0        4C3BC0               cmp      r8, rax        751D                 jne      SHORT G_M000_IG07        446BFA2A             imul     r15d, edx, 42 

这是从委托中加载目标地址到r8,并加载预期目标的地址到rax。如果它们相同,它就简单地执行内联操作(imul r15d, edx, 42),否则就跳转到G_M000_IG07,调用r8的函数。如果我们把它作为一个基准运行,其效果是显而易见的。

static int[] s_values = Enumerable.Range(0, 1_000).ToArray();  [Benchmark] public int DelegatePGO() => Sum(s_values, i => i * 42);  static int Sum(int[] values, Func<int, int>? func) {     int sum = 0;     foreach (int value in values)     {         sum += func(value);     }     return sum; } 

在禁用PGO的情况下,我们在.NET 6和.NET 7中得到了相同的性能吞吐量。

方法 运行时间 平均值 比率
DelegatePGO .NET 6.0 1.665 us 1.00
DelegatePGO .NET 7.0 1.659 us 1.00

但当我们启用动态PGO(DOTNET_TieredPGO=1)时,情况发生了变化。.NET 6的速度提高了~14%,但.NET 7的速度提高了~3倍!

方法 运行时间 平均值 比率
DelegatePGO .NET 6.0 1,427.7 ns 1.00
DelegatePGO .NET 7.0 539.0 ns 0.38

dotnet/runtime#70377是动态PGO的另一个有价值的改进,它使PGO能够很好地发挥循环克隆和不变量提升的作用。为了更好地理解这一点,简要地说说这些是什么。循环克隆 (Loop cloning) 是JIT采用的一种机制,以避免循环的快速路径中的各种开销。考虑一下本例中的Test方法。

using System.Runtime.CompilerServices;  class Program {     static void Main()     {         int[] array = new int[10_000_000];         for (int i = 0; i < 1_000_000; i++)         {             Test(array);         }     }      [MethodImpl(MethodImplOptions.NoInlining)]     private static bool Test(int[] array)     {         for (int i = 0; i < 0x12345; i++)         {             if (array[i] == 42)             {                 return true;             }         }          return false;     } } 

JIT不知道传入的数组是否有足够的长度,以至于在循环中对数组[i]的所有访问都在边界内,因此它需要为每次访问注入边界检查。虽然简单地在前面进行长度检查,并在长度不够的情况下提前抛出一个异常是很好的,但这样做也会改变行为(设想该方法在进行时向数组中写入数据,或者以其他方式改变一些共享状态)。相反,JIT采用了 "循环克隆"。它从本质上重写了这个测试方法,使之更像这样。

if (array is not null && array.Length >= 0x12345) {     for (int i = 0; i < 0x12345; i++)     {         if (array[i] == 42) // no bounds checks emitted for this access :-)         {             return true;         }     } } else {     for (int i = 0; i < 0x12345; i++)     {         if (array[i] == 42) // bounds checks emitted for this access :-(         {             return true;         }     } } return false; 

这样一来,以一些代码重复为代价,我们得到了没有边界检查的快速循环,而只需支付慢速路径中的边界检查。你可以在生成的程序集中看到这一点(如果你还不明白,DOTNET_JitDisasm是.NET 7中我最喜欢的功能之一)。

; Assembly listing for method Program:Test(ref):bool ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; rsp based frame ; fully interruptible ; No PGO data  G_M000_IG01:                ;; offset=0000H        4883EC28             sub      rsp, 40  G_M000_IG02:                ;; offset=0004H        33C0                 xor      eax, eax        4885C9               test     rcx, rcx        7429                 je       SHORT G_M000_IG05        81790845230100       cmp      dword ptr [rcx+08H], 0x12345        7C20                 jl       SHORT G_M000_IG05        0F1F40000F1F840000000000 align    [12 bytes for IG03]  G_M000_IG03:                ;; offset=0020H        8BD0                 mov      edx, eax        837C91102A           cmp      dword ptr [rcx+4*rdx+10H], 42        7429                 je       SHORT G_M000_IG08        FFC0                 inc      eax        3D45230100           cmp      eax, 0x12345        7CEE                 jl       SHORT G_M000_IG03  G_M000_IG04:                ;; offset=0032H        EB17                 jmp      SHORT G_M000_IG06  G_M000_IG05:                ;; offset=0034H        3B4108               cmp      eax, dword ptr [rcx+08H]        7323                 jae      SHORT G_M000_IG10        8BD0                 mov      edx, eax        837C91102A           cmp      dword ptr [rcx+4*rdx+10H], 42        7410                 je       SHORT G_M000_IG08        FFC0                 inc      eax        3D45230100           cmp      eax, 0x12345        7CE9                 jl       SHORT G_M000_IG05  G_M000_IG06:                ;; offset=004BH        33C0                 xor      eax, eax  G_M000_IG07:                ;; offset=004DH        4883C428             add      rsp, 40        C3                   ret  G_M000_IG08:                ;; offset=0052H        B801000000           mov      eax, 1  G_M000_IG09:                ;; offset=0057H        4883C428             add      rsp, 40        C3                   ret  G_M000_IG10:                ;; offset=005CH        E81FA0C15F           call     CORINFO_HELP_RNGCHKFAIL        CC                   int3  ; Total bytes of code 98 

G_M000_IG02部分正在进行空值检查和长度检查,如果任何一项失败,则跳转到G_M000_IG05块。如果两者都成功了,它就会执行循环(G_M000_IG03块)而不进行边界检查。

G_M000_IG03:                ;; offset=0020H        8BD0                 mov      edx, eax        837C91102A           cmp      dword ptr [rcx+4*rdx+10H], 42        7429                 je       SHORT G_M000_IG08        FFC0                 inc      eax        3D45230100           cmp      eax, 0x12345        7CEE                 jl       SHORT G_M000_IG03 

边界检查只显示在慢速路径块中。

G_M000_IG05:                ;; offset=0034H        3B4108               cmp      eax, dword ptr [rcx+08H]        7323                 jae      SHORT G_M000_IG10        8BD0                 mov      edx, eax        837C91102A           cmp      dword ptr [rcx+4*rdx+10H], 42        7410                 je       SHORT G_M000_IG08        FFC0                 inc      eax        3D45230100           cmp      eax, 0x12345        7CE9                 jl       SHORT G_M000_IG05 

这就是 "循环克隆"。那么,"不变量提升 (invariant hoisting) "呢?提升是指把某个东西从循环中拉到循环之前,而不变量是不会改变的东西。因此,不变量提升是指把某个东西从循环中拉到循环之前,以避免在循环的每个迭代中重新计算一个不会改变的答案。实际上,前面的例子已经展示了不变量提升,即边界检查被移到了循环之前,而不是在循环中,但一个更具体的例子是这样的。

[MethodImpl(MethodImplOptions.NoInlining)] private static bool Test(int[] array) {     for (int i = 0; i < 0x12345; i++)     {         if (array[i] == array.Length - 42)         {             return true;         }     }      return false; } 

注意,array.Length - 42的值在循环的每次迭代中都不会改变,所以它对循环迭代是 "不变的",可以被抬出来,生成的代码就是这样做的。

G_M000_IG02:                ;; offset=0004H        33D2                 xor      edx, edx        4885C9               test     rcx, rcx        742A                 je       SHORT G_M000_IG05        448B4108             mov      r8d, dword ptr [rcx+08H]        4181F845230100       cmp      r8d, 0x12345        7C1D                 jl       SHORT G_M000_IG05        4183C0D6             add      r8d, -42        0F1F4000             align    [4 bytes for IG03]  G_M000_IG03:                ;; offset=0020H        8BC2                 mov      eax, edx        4439448110           cmp      dword ptr [rcx+4*rax+10H], r8d        7433                 je       SHORT G_M000_IG08        FFC2                 inc      edx        81FA45230100         cmp      edx, 0x12345        7CED                 jl       SHORT G_M000_IG03 

这里我们再次看到数组被测试为空(test rcx, rcx),数组的长度被检查(mov r8d, dword ptr [rcx+08H] then cmp r8d, 0x12345),但是在r8d中有数组的长度,然后我们看到这个前期块从长度中减去42(add r8d, -42),这是在我们继续进入G_M000_IG03块的快速路径循环前。这使得额外的操作集不在循环中,从而避免了每次迭代重新计算数值的开销。

好的,那么这如何适用于动态PGO呢?请记住,对于PGO能够做到的界面/虚拟调度的规避,它是通过进行类型检查,看使用的类型是否是最常见的类型;如果是,它就使用直接调用该类型方法的快速路径(这样做的话,该调用有可能被内联),如果不是,它就回到正常的界面/虚拟调度。这种检查可以不受循环的影响。因此,当一个方法被分层,PGO启动时,类型检查现在可以从循环中提升出来,使得处理普通情况更加便宜。考虑一下我们原来的例子的这个变化。

using System.Runtime.CompilerServices;  class Program {     static void Main()     {         IPrinter printer = new BlankPrinter();         while (true)         {             DoWork(printer);         }     }      [MethodImpl(MethodImplOptions.NoInlining)]     static void DoWork(IPrinter printer)     {         for (int j = 0; j < 123; j++)         {             printer.Print(j);         }     }      interface IPrinter     {         void Print(int i);     }      class BlankPrinter : IPrinter     {         public void Print(int i)         {             Console.Write("");         }     } } 

当我们看一下在启用动态PGO的情况下为其生成的优化程序集时,我们看到了这个。

; Assembly listing for method Program:DoWork(IPrinter) ; Emitting BLENDED_CODE for X64 CPU with AVX - Windows ; Tier-1 compilation ; optimized code ; optimized using profile data ; rsp based frame ; partially interruptible ; with Dynamic PGO: edge weights are invalid, and fgCalledCount is 12187 ; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data  G_M000_IG01:                ;; offset=0000H        57                   push     rdi        56                   push     rsi        4883EC28             sub      rsp, 40        488BF1               mov      rsi, rcx  G_M000_IG02:                ;; offset=0009H        33FF                 xor      edi, edi        4885F6               test     rsi, rsi        742B                 je       SHORT G_M000_IG05        48B9982DD43CFC7F0000 mov      rcx, 0x7FFC3CD42D98        48390E               cmp      qword ptr [rsi], rcx        751C                 jne      SHORT G_M000_IG05  G_M000_IG03:                ;; offset=001FH        48B9282040F948020000 mov      rcx, 0x248F9402028        488B09               mov      rcx, gword ptr [rcx]        FF1526A80D00         call     [Console:Write(String)]        FFC7                 inc      edi        83FF7B               cmp      edi, 123        7CE6                 jl       SHORT G_M000_IG03  G_M000_IG04:                ;; offset=0039H        EB29                 jmp      SHORT G_M000_IG07  G_M000_IG05:                ;; offset=003BH        48B9982DD43CFC7F0000 mov      rcx, 0x7FFC3CD42D98        48390E               cmp      qword ptr [rsi], rcx        7521                 jne      SHORT G_M000_IG08        48B9282040F948020000 mov      rcx, 0x248F9402028        488B09               mov      rcx, gword ptr [rcx]        FF15FBA70D00         call     [Console:Write(String)]  G_M000_IG06:                ;; offset=005DH        FFC7                 inc      edi        83FF7B               cmp      edi, 123        7CD7                 jl       SHORT G_M000_IG05  G_M000_IG07:                ;; offset=0064H        4883C428             add      rsp, 40        5E                   pop      rsi        5F                   pop      rdi        C3                   ret  G_M000_IG08:                ;; offset=006BH        488BCE               mov      rcx, rsi        8BD7                 mov      edx, edi        49BB1000AA3CFC7F0000 mov      r11, 0x7FFC3CAA0010        41FF13               call     [r11]IPrinter:Print(int):this        EBDE                 jmp      SHORT G_M000_IG06  ; Total bytes of code 127 

我们可以在G_M000_IG02块中看到,它正在对IPrinter实例进行类型检查,如果检查失败就跳到G_M000_IG05(mov rcx, 0x7FFC3CD42D98 then cmp qword ptr [rsi], rcx then jne SHORT G_M000_IG05),否则就跳到G_M000_IG03,这是一个紧密的快速路径循环,内联BlankPrinter.Print,看不到任何类型检查。

有趣的是,这样的改进也会带来自己的挑战。PGO导致了类型检查数量的大幅增加,因为专门针对某一特定类型的调用站点需要与该类型进行比较。然而,普通的子表达式消除 (common subexpression elimination)(CSE)在历史上并不适用这种类型的句柄(CSE是一种编译器优化,通过计算一次结果,然后存储起来供以后使用,而不是每次都重新计算,来消除重复的表达式)。dotnet/runtime#70580通过对这种常量句柄启用CSE来解决这个问题。例如,考虑这个方法。

[Benchmark] [Arguments("", "", "", "")] public bool AllAreStrings(object o1, object o2, object o3, object o4) =>     o1 is string && o2 is string && o3 is string && o4 is string; 

在.NET 6上,JIT产生了这个汇编代码:

; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object)        test      rdx,rdx        je        short M00_L01        mov       rax,offset MT_System.String        cmp       [rdx],rax        jne       short M00_L01        test      r8,r8        je        short M00_L01        mov       rax,offset MT_System.String        cmp       [r8],rax        jne       short M00_L01        test      r9,r9        je        short M00_L01        mov       rax,offset MT_System.String        cmp       [r9],rax        jne       short M00_L01        mov       rax,[rsp+28]        test      rax,rax        je        short M00_L00        mov       rdx,offset MT_System.String        cmp       [rax],rdx        je        short M00_L00        xor       eax,eax M00_L00:        test      rax,rax        setne     al        movzx     eax,al        ret M00_L01:        xor       eax,eax        ret ; Total bytes of code 100 

请注意,C#对字符串有四个测试,而汇编代码中的mov rax,offset MT_System.String有四个加载。现在在.NET 7上,加载只执行一次。

; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object)        test      rdx,rdx        je        short M00_L01        mov       rax,offset MT_System.String        cmp       [rdx],rax        jne       short M00_L01        test      r8,r8        je        short M00_L01        cmp       [r8],rax        jne       short M00_L01        test      r9,r9        je        short M00_L01        cmp       [r9],rax        jne       short M00_L01        mov       rdx,[rsp+28]        test      rdx,rdx        je        short M00_L00        cmp       [rdx],rax        je        short M00_L00        xor       edx,edx M00_L00:        xor       eax,eax        test      rdx,rdx        setne     al        ret M00_L01:        xor       eax,eax        ret ; Total bytes of code 69 

原文链接

Performance Improvements in .NET 7

【译】.NET 7 中的性能改进(三)

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com)

发表评论

相关文章