百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

在.Net 7中性能改进-PGO(按配置优化)

liebian365 2024-11-27 17:08 2 浏览 0 评论

前言

本文是 Performance Improvements in .NET 7 PGO部分的翻译.下面开始正文:

//原文地址: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/

我在我的 Performance Improvements in .NET 6 文章中写过关于配置文件引导优化(PGO)的内容,但在这里我将再次介绍它,因为它为.net 7带来了大量的改进.

// Performance Improvements in .NET 6 
//地址: https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-6/

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

动态PGO利用了分层编译的优势.我注意到JIT利用0层代码来跟踪方法被调用了多少次,或者在循环的情况下,跟踪循环执行了多少次.它也可以用它来做其他事情.例如,它可以准确地跟踪将哪些具体类型用作接口分发的目标,然后在第1层专门化代码以期望最常见的类型(这被称为“受保护的去虚拟化”或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的tier-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

值得注意的是,你可以看到调用call [r11]IPrinter:PrintIfTrue(bool):this做接口分发.但是,接下来看看为第1层生成的代码.我们仍然看到call [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的调用)内联版本.因此,通常的情况(本例中唯一的情况)是以单个比较和分支为代价的超级高效.

在.NET6中已经有了,那么为什么我们现在要讨论它呢?有几个方面有所改善.首先,由于dotnet/runtime#61453等改进,PGO现在与OSR一起工作.这是一件大事,因为这意味着执行这种接口分发(这是相当常见的)的长时间运行的热方法可以获得这种类型的去虚拟化/内联优化.第二,虽然PGO目前没有默认启用,但我们已经让它更容易启用.在dotnet/runtime#71438和dotnet/sdk#26350之间,现在可以简单地把 <TieredPGO>true</TieredPGO> 在你的项目工程文件(*.csproj)中,它会有相同的效果,如果你设置DOTNET_TieredPGO=1之前的应用程序调用,启用动态PGO(注意,它不禁用R2R镜像,所以如果你想要整个核心库也采用动态PGO,你还需要设置DOTNET_ReadyToRun=0).第三,动态PGO已经学会了如何测量和优化附加的东西.

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

注意 call [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上获得了相同的性能吞吐量:


Method

Runtime

Mean

Ratio

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倍!

Method

Runtime

Mean

Ratio

DelegatePGO

.NET 6.0

1,427.7 ns

1.00

DelegatePGO

.NET 7.0

539.0 ns

0.38

dotnet/runtime#70377是动态PGO的另一个有价值的改进,它使PGO能够很好地进行循环克隆和不变量提升.为了更好地理解这一点,我们稍微离题一下这些是什么.循环克隆是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不知道传入的数组是否足够长,使得循环中所有对array[i]的访问都在边界内,因此它需要为每次访问注入边界检查.虽然在前面简单地进行长度检查并在长度不够时提前抛出异常是一件好事,但这样做也会改变行为(想象一下这个方法正在写入数组,或者改变一些共享状态).相反,JIT使用“循环克隆”.它本质上重写了Test方法,使其更像这样:

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块执行null检查和长度检查,如果失败,则跳转到G_M000_IG05块.如果两者都成功了,它就会执行循环(block 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

这是“循环克隆”.那什么是"不变提升”呢? 提升是把某个东西从循环中拉出来放到循环之前,不变量是不变的东西.因此,不变提升是在循环之前从循环中拉出一些东西,以避免在每次循环迭代中重新计算一个不变的答案.实际上,前面的例子已经展示了不变量提升,因为边界检查被移动到循环之前,而不是在循环中,但一个更具体的例子应该是这样的:

[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

在这里,我们再次检查数组是否为null( test rcx, rcx )和数组的长度被检查( mov r8d, dword ptr [rcx+08H] 然后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 然后( cmp qword ptr [rsi], rcx 然后 jne SHORT G_M000_IG05 ),否则就跳转到G_M000_IG03,这是一个紧密的快速路径循环,内联 BlankPrinter.Print 后没有进行类型检查!

有趣的是,这样的改进也会带来挑战.PGO导致类型检查数量的显著增加,因为专门化给定类型的调用站点需要与该类型进行比较.然而,常见的子表达式消除(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 ;;第1次加载
       cmp    [rdx],rax
       jne       short M00_L01
       test      r8,r8
       je         short M00_L01
       mov     rax,offset MT_System.String  ;;第2次加载
       cmp     [r8],rax
       jne       short M00_L01
       test      r9,r9
       je         short M00_L01
       mov     rax,offset MT_System.String  ;;第3次加载
       cmp     [r9],rax
       jne       short M00_L01
       mov     rax,[rsp+28]
       test      rax,rax
       je         short M00_L00
       mov     rdx,offset MT_System.String  ;;第4次加载
       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#有四个string(字符串)test(逻辑与运算,汇编代码有四个加载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  ;;只有1次加载
       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

因JIT部分内容太多,这里进行拆分,PGO就到了,Bounds Check Elimination(消除边界检查)拆分为一篇博文.

个人能力有限,如果您发现有什么不对,请私信我

如果您觉得对您有用的话,可以点个赞或者加个关注,欢迎大家一起进行技术交流

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: