3

【译】.NET 7 中的性能改进(三) - 郑子铭

 1 year ago
source link: https://www.cnblogs.com/MingsonZheng/p/17142946.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

原文 | Stephen Toub

翻译 | 郑子铭

我在我的 .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

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

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

如有任何疑问,请与我联系 ([email protected])


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK