天天看点

在.Net 7性能改进-栈上替换(OSR)

作者:秋风技术

前言

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

//原文地址: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#loop-hoisting-and-cloning           

On-Stack Replacement(栈上替换),是在.NET 7中实现JIT的最酷的功能之一.但要真正理解OSR,我们首先需要理解分层编译,所以快速回顾一下…

使用JIT编译器的托管环境必须处理的问题之一是启动和吞吐量之间的权衡.从历史上看,编译器优化的任务是生成执行更快的代码,以便在应用程序或服务运行时实现尽可能最佳的吞吐量.但这种优化需要分析,需要时间,执行所有这些工作会导致启动时间增加,因为程序的所有代码(例如,在web服务器可以服务第一个请求之前需要运行的所有代码)都需要编译.因此,JIT编译器需要做出权衡:以更长的启动时间为代价提高吞吐量,或以降低吞吐量为代价提高启动时间.对于某些类型的应用程序和服务,折衷是一个简单的调用,例如,如果您的服务启动一次,然后运行几天,额外几秒的启动时间并不重要,或者如果您是一个控制台应用程序,将要进行快速计算并退出,启动时间才是最重要的.

但是,JIT如何知道它处于哪个场景中,我们真的希望每个开发人员都知道这些设置和权衡,并相应地配置他们的每个应用程序吗?对此的一个答案是提前编译,它在.NET中采用了多种形式.例如,所有的核心库都是“crossgen”,这意味着它们已经通过一个工具运行,该工具生成了前面提到的R2R格式,生成的二进制文件包含汇编代码,只需要稍加调整即可实际执行;不是每个方法都可以为其生成代码,但足以显著减少启动时间.当然,这种方法也有其自身的缺点,例如,JIT编译器的一个承诺是,它可以利用当前机器/进程的知识进行最佳优化,因此,例如,R2R映像必须假设某个基线指令集(例如,哪些矢量化指令可用),而JIT可以看到哪些实际可用并使用最佳.“分层编译”提供了另一个答案,无论是否使用这些其他提前(AOT)编译解决方案,都可以使用.

分层编译使JIT能够鱼与熊掌兼得.分层编译这个想法很简单:允许JIT多次编译相同的代码.第一次,JIT可以使用尽可能少的优化(少量优化实际上可以使JIT自身的吞吐量更快,因此这些优化仍然适用),生成相当未优化的汇编代码,但速度非常快.当它这样做时,它可以在程序集中添加一些工具来跟踪方法的调用频率.事实证明,在启动路径上使用的许多函数被调用一次,或者可能只调用了几次,优化它们比不优化执行它们需要更多的时间.然后,当方法的插装触发某个阈值时,例如,一个方法已执行30次,工作项将排队重新编译该方法,但这一次JIT可以对其进行所有优化.这被亲切地称为“分层”.一旦重新编译完成,方法的调用站点就会用新高度优化的汇编代码的地址进行修补,未来的调用将采用快速路径.因此,我们获得了更快的启动和更快的持续吞吐量.

然而,一个问题是不适合这种模式的方法.当然,许多性能敏感的方法都相对较快,执行了很多次,但也有大量性能敏感方法只执行了几次,甚至可能只执行一次,但执行需要很长时间,甚至可能是整个过程的持续时间:带循环的方法.因此,默认情况下,分层编译未应用于循环,但可以通过将DOTNET_TC_QuickJitForLoops环境变量设置为1来启用.我们可以通过尝试使用.NET 6的简单控制台应用程序来查看其效果.使用默认设置,运行此应用程序:

class Program
{
    static void Main()
    {
        var sw = new System.Diagnostics.Stopwatch();
        while (true)
        {
            sw.Restart();
            for (int trial = 0; trial < 10_000; trial++)
            {
                int count = 0;
                for (int i = 0; i < char.MaxValue; i++)
                    if (IsAsciiDigit((char)i))
                        count++;
            }
            sw.Stop();
            Console.WriteLine(sw.Elapsed);
        }

        static bool IsAsciiDigit(char c) => (uint)(c - '0') <= 9;
    }
}           

输出内容:

00:00:00.5734352
00:00:00.5526667
00:00:00.5675267
00:00:00.5588724
00:00:00.5616028           

现在,尝试将DOTNET_TC_QuickJitForLoops设置为1.当我再次运行它时,得到如下数字:

00:00:01.2841397
00:00:01.2693485
00:00:01.2755646
00:00:01.2656678
00:00:01.2679925           

换句话说,启用DOTNET_TC_QuickJitForLoops时,需要的时间是不启用时的2.5倍(在.NET6中).这是因为这个主函数从未应用过优化.通过将DOTNET_TC_QuickJitForLoops设置为1,我们说“JIT,请将分层也应用于具有循环的方法”,但这种具有循环的方式只调用一次,因此在整个过程中,它最终保持在“tier-0”,即未优化.现在,让我们对.NET 7进行同样的尝试.无论是否设置了环境变量,我都会再次得到如下数字:

00:00:00.5528889
00:00:00.5562563
00:00:00.5622086
00:00:00.5668220
00:00:00.5589112           

但重要的是,这种方法仍然参与了分层.事实上,我们可以通过使用前面提到的DOTNET_JitDisasmSummary=1环境变量来确认.当我设置并再次运行时,我在输出中看到这些行:

4: JIT compiled Program:Main() [Tier0, IL size=83, code size=319]
...
6: JIT compiled Program:Main() [Tier1-OSR @0x27, IL size=83, code size=380]           

栈上替换的思想是,方法不仅可以在调用之间替换,而且可以在“栈上”执行时替换.除了为调用计数检测第0层代码外,还为迭代计数检测循环.当迭代超过某个限制时,JIT编译该方法的新的高度优化版本,将所有本地/寄存器状态从当前调用转移到新调用,然后跳到新方法中的适当位置.通过使用前面讨论的DOTNET_JitDisasm环境变量,我们可以看到这一点.将其设置为Program:*以查看为Program类中的所有方法生成的汇编代码,然后再次运行应用程序.您应该看到如下输出:

// Assembly listing for method Program:Main()
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-0 compilation
// MinOpts code
// rbp based frame
// partially interruptible

G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       4881EC80000000       sub      rsp, 128
       488DAC2480000000     lea      rbp, [rsp+80H]
       C5D857E4             vxorps   xmm4, xmm4
       C5F97F65B0           vmovdqa  xmmword ptr [rbp-50H], xmm4
       33C0                 xor      eax, eax
       488945C0             mov      qword ptr [rbp-40H], rax

G_M000_IG02:                ;; offset=001FH
       48B9002F0B50FC7F0000 mov      rcx, 0x7FFC500B2F00
       E8721FB25F           call     CORINFO_HELP_NEWSFAST
       488945B0             mov      gword ptr [rbp-50H], rax
       488B4DB0             mov      rcx, gword ptr [rbp-50H]
       FF1544C70D00         call     [Stopwatch:.ctor():this]
       488B4DB0             mov      rcx, gword ptr [rbp-50H]
       48894DC0             mov      gword ptr [rbp-40H], rcx
       C745A8E8030000       mov      dword ptr [rbp-58H], 0x3E8

G_M000_IG03:                ;; offset=004BH
       8B4DA8               mov      ecx, dword ptr [rbp-58H]
       FFC9                 dec      ecx
       894DA8               mov      dword ptr [rbp-58H], ecx
       837DA800             cmp      dword ptr [rbp-58H], 0
       7F0E                 jg       SHORT G_M000_IG05

G_M000_IG04:                ;; offset=0059H
       488D4DA8             lea      rcx, [rbp-58H]
       BA06000000           mov      edx, 6
       E8B985AB5F           call     CORINFO_HELP_PATCHPOINT

G_M000_IG05:                ;; offset=0067H
       488B4DC0             mov      rcx, gword ptr [rbp-40H]
       3909                 cmp      dword ptr [rcx], ecx
       FF1585C70D00         call     [Stopwatch:Restart():this]
       33C9                 xor      ecx, ecx
       894DBC               mov      dword ptr [rbp-44H], ecx
       33C9                 xor      ecx, ecx
       894DB8               mov      dword ptr [rbp-48H], ecx
       EB20                 jmp      SHORT G_M000_IG08

G_M000_IG06:                ;; offset=007FH
       8B4DB8               mov      ecx, dword ptr [rbp-48H]
       0FB7C9               movzx    rcx, cx
       FF152DD40B00         call     [Program:<Main>g__IsAsciiDigit|0_0(ushort):bool]
       85C0                 test     eax, eax
       7408                 je       SHORT G_M000_IG07
       8B4DBC               mov      ecx, dword ptr [rbp-44H]
       FFC1                 inc      ecx
       894DBC               mov      dword ptr [rbp-44H], ecx

G_M000_IG07:                ;; offset=0097H
       8B4DB8               mov      ecx, dword ptr [rbp-48H]
       FFC1                 inc      ecx
       894DB8               mov      dword ptr [rbp-48H], ecx

G_M000_IG08:                ;; offset=009FH
       8B4DA8               mov      ecx, dword ptr [rbp-58H]
       FFC9                 dec      ecx
       894DA8               mov      dword ptr [rbp-58H], ecx
       837DA800             cmp      dword ptr [rbp-58H], 0
       7F0E                 jg       SHORT G_M000_IG10

G_M000_IG09:                ;; offset=00ADH
       488D4DA8             lea      rcx, [rbp-58H]
       BA23000000           mov      edx, 35
       E86585AB5F           call     CORINFO_HELP_PATCHPOINT

G_M000_IG10:                ;; offset=00BBH
       817DB800CA9A3B       cmp      dword ptr [rbp-48H], 0x3B9ACA00
       7CBB                 jl       SHORT G_M000_IG06
       488B4DC0             mov      rcx, gword ptr [rbp-40H]
       3909                 cmp      dword ptr [rcx], ecx
       FF1570C70D00         call     [Stopwatch:get_ElapsedMilliseconds():long:this]
       488BC8               mov      rcx, rax
       FF1507D00D00         call     [Console:WriteLine(long)]
       E96DFFFFFF           jmp      G_M000_IG03

// Total bytes of code 222

// Assembly listing for method Program:<Main>g__IsAsciiDigit|0_0(ushort):bool
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-0 compilation
// MinOpts code
// rbp based frame
// partially interruptible

G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       488BEC               mov      rbp, rsp
       894D10               mov      dword ptr [rbp+10H], ecx

G_M000_IG02:                ;; offset=0007H
       8B4510               mov      eax, dword ptr [rbp+10H]
       0FB7C0               movzx    rax, ax
       83C0D0               add      eax, -48
       83F809               cmp      eax, 9
       0F96C0               setbe    al
       0FB6C0               movzx    rax, al

G_M000_IG03:                ;; offset=0019H
       5D                   pop      rbp
       C3                   ret                

这里有一些相关的事情需要注意.首先,顶部的注释强调了这段代码是如何编译的:

// Tier-0 compilation
// MinOpts code           

因此,我们知道这是用最小优化(“MinOpts”)编译的方法的初始版本(“第0层”).第二,注意装配的这一行:

FF152DD40B00         call     [Program:<Main>g__IsAsciiDigit|0_0(ushort):bool]           

我们的IsAsciiDigit辅助方法是简单的可内联的,但它没有内联;相反,程序集有一个对它的调用,事实上,我们可以在下面看到为IsAsciiDigit生成的代码(也是“MinOpts”).为什么?因为内联是一种优化(非常重要的优化),但在tier-0中被禁用(因为为了进行良好的内联分析也非常昂贵).第三,我们可以看到JIT输出到检测该方法的代码.这有点复杂,但我会指出相关部分.首先,我们看到:

C745A8E8030000       mov      dword ptr [rbp-58H], 0x3E8           

0x3E8是十进制1000的十六进制值,这是在JIT生成方法的优化版本之前循环需要迭代的默认迭代次数(可通过环境变量DOTNET_TC_OnStackReplacement_InitialCounter进行配置).因此,我们看到1000存储在这个堆栈位置.然后,在该方法的后面,我们看到:

G_M000_IG03:                // offset=004BH
       8B4DA8               mov      ecx, dword ptr [rbp-58H]
       FFC9                 dec      ecx
       894DA8               mov      dword ptr [rbp-58H], ecx
       837DA800             cmp      dword ptr [rbp-58H], 0
       7F0E                 jg       SHORT G_M000_IG05

G_M000_IG04:                // offset=0059H
       488D4DA8             lea      rcx, [rbp-58H]
       BA06000000           mov      edx, 6
       E8B985AB5F           call     CORINFO_HELP_PATCHPOINT

G_M000_IG05:                // offset=0067H           

生成的代码将该计数器加载到ecx寄存器中,将其递减,存储回,然后查看计数器是否下降到0.如果没有下降,则代码跳转到G_M000_IG05,这是循环其余部分中实际代码的标签.但如果计数器下降到0,JIT将继续将相关状态存储到rcx和edx寄存器中,然后调用CORINFO_HELP_PATCHPOINT helper方法.该助手负责触发优化方法的创建(如果它还不存在),修复所有适当的跟踪状态,并跳转到新方法.事实上,如果您再次查看运行程序的控制台输出,您将看到主方法的另一个输出:

// Assembly listing for method Program:Main()
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-1 compilation
// OSR variant for entry point 0x23
// optimized code
// rsp based frame
// fully interruptible
// No PGO data
// 1 inlinees with PGO data; 8 single block inlinees; 0 inlinees without PGO data

G_M000_IG01:                // offset=0000H
       4883EC58             sub      rsp, 88
       4889BC24D8000000     mov      qword ptr [rsp+D8H], rdi
       4889B424D0000000     mov      qword ptr [rsp+D0H], rsi
       48899C24C8000000     mov      qword ptr [rsp+C8H], rbx
       C5F877               vzeroupper
       33C0                 xor      eax, eax
       4889442428           mov      qword ptr [rsp+28H], rax
       4889442420           mov      qword ptr [rsp+20H], rax
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       8BBC249C000000       mov      edi, dword ptr [rsp+9CH]
       8BB42498000000       mov      esi, dword ptr [rsp+98H]

G_M000_IG02:                // offset=0041H
       EB45                 jmp      SHORT G_M000_IG05
                            align    [0 bytes for IG06]

G_M000_IG03:                // offset=0043H
       33C9                 xor      ecx, ecx
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       48894B08             mov      qword ptr [rbx+08H], rcx
       488D4C2428           lea      rcx, [rsp+28H]
       48B87066E68AFD7F0000 mov      rax, 0x7FFD8AE66670

G_M000_IG04:                // offset=0060H
       FFD0                 call     rax ; Kernel32:QueryPerformanceCounter(long):int
       488B442428           mov      rax, qword ptr [rsp+28H]
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       48894310             mov      qword ptr [rbx+10H], rax
       C6431801             mov      byte  ptr [rbx+18H], 1
       33FF                 xor      edi, edi
       33F6                 xor      esi, esi
       833D92A1E55F00       cmp      dword ptr [(reloc 0x7ffcafe1ae34)], 0
       0F85CA000000         jne      G_M000_IG13

G_M000_IG05:                // offset=0088H
       81FE00CA9A3B         cmp      esi, 0x3B9ACA00
       7D17                 jge      SHORT G_M000_IG09

G_M000_IG06:                // offset=0090H
       0FB7CE               movzx    rcx, si
       83C1D0               add      ecx, -48
       83F909               cmp      ecx, 9
       7702                 ja       SHORT G_M000_IG08

G_M000_IG07:                // offset=009BH
       FFC7                 inc      edi

G_M000_IG08:                // offset=009DH
       FFC6                 inc      esi
       81FE00CA9A3B         cmp      esi, 0x3B9ACA00
       7CE9                 jl       SHORT G_M000_IG06

G_M000_IG09:                // offset=00A7H
       488B6B08             mov      rbp, qword ptr [rbx+08H]
       48899C24A0000000     mov      gword ptr [rsp+A0H], rbx
       807B1800             cmp      byte  ptr [rbx+18H], 0
       7436                 je       SHORT G_M000_IG12

G_M000_IG10:                // offset=00B9H
       488D4C2420           lea      rcx, [rsp+20H]
       48B87066E68AFD7F0000 mov      rax, 0x7FFD8AE66670

G_M000_IG11:                // offset=00C8H
       FFD0                 call     rax ; Kernel32:QueryPerformanceCounter(long):int
       488B4C2420           mov      rcx, qword ptr [rsp+20H]
       488B9C24A0000000     mov      rbx, gword ptr [rsp+A0H]
       482B4B10             sub      rcx, qword ptr [rbx+10H]
       4803E9               add      rbp, rcx
       833D2FA1E55F00       cmp      dword ptr [(reloc 0x7ffcafe1ae34)], 0
       48899C24A0000000     mov      gword ptr [rsp+A0H], rbx
       756D                 jne      SHORT G_M000_IG14

G_M000_IG12:               // offset=00EFH
       C5F857C0             vxorps   xmm0, xmm0
       C4E1FB2AC5           vcvtsi2sd  xmm0, rbp
       C5FB11442430         vmovsd   qword ptr [rsp+30H], xmm0
       48B9F04BF24FFC7F0000 mov      rcx, 0x7FFC4FF24BF0
       BAE7070000           mov      edx, 0x7E7
       E82E1FB25F           call     CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
       C5FB10442430         vmovsd   xmm0, qword ptr [rsp+30H]
       C5FB5905E049F6FF     vmulsd   xmm0, xmm0, qword ptr [(reloc 0x7ffc4ff25720)]
       C4E1FB2CD0           vcvttsd2si  rdx, xmm0
       48B94B598638D6C56D34 mov      rcx, 0x346DC5D63886594B
       488BC1               mov      rax, rcx
       48F7EA               imul     rdx:rax, rdx
       488BCA               mov      rcx, rdx
       48C1E93F             shr      rcx, 63
       48C1FA0B             sar      rdx, 11
       4803CA               add      rcx, rdx
       FF1567CE0D00         call     [Console:WriteLine(long)]
       E9F5FEFFFF           jmp      G_M000_IG03

G_M000_IG13:               // offset=014EH
       E8DDCBAC5F           call     CORINFO_HELP_POLL_GC
       E930FFFFFF           jmp      G_M000_IG05

G_M000_IG14:               // offset=0158H
       E8D3CBAC5F           call     CORINFO_HELP_POLL_GC
       EB90                 jmp      SHORT G_M000_IG12

// Total bytes of code 351           

这里,我们再次注意到一些有趣的事情.首先,在头文件中我们看到:

// Tier-1 compilation
// OSR variant for entry point 0x23
// optimized code           

因此,我们知道这既是优化的“第1层”代码,也是该方法的“OSR变体”.其次,请注意,不再调用IsAsciiDigit方法.相反,我们看到的是,该调用的位置:

G_M000_IG06:                ;; offset=0090H
       0FB7CE               movzx    rcx, si
       83C1D0               add      ecx, -48
       83F909               cmp      ecx, 9
       7702                 ja       SHORT G_M000_IG08           

这是将一个值加载到rcx中,从中减去48(48是“0”字符的十进制ASCII值),并将结果值与9进行比较.听起来很像我们的IsasciidGit实现( (uint)(c-“0”)<=9 ),不是吗?这是因为它是.帮助程序成功地内联到现在优化的代码中.

很好,现在在.NET7中,我们可以在很大程度上避免启动和吞吐量之间的权衡,因为OSR使分层编译能够应用于所有方法,甚至是那些长期运行的方法.许多提交开始启用此功能,包括过去几年中的许多提交,但是所有的功能在发布时都被禁用了.由于dotnet/runtime#62831在Arm64上实现了对OSR的支持(之前仅实现了x64支持),以及dotnet/Runtime#63406和dotnet/runtime#65609修改了如何OSR导入和epilog的处理,dotnet/runtime#65675在默认情况下启用OSR(DOTNET_TC_QuickJitForLoops=1).

但是,分层编译和OSR不仅仅是关于启动(尽管它们在那里当然非常有价值).他们还将进一步提高吞吐量.尽管分层编译最初被设想为一种在不影响吞吐量的情况下优化启动的方法,但它已经远远不止于此.JIT可以在tier-0期间了解到关于方法的各种信息,然后可以用于tier-1.例如,执行的tier-2代码意味着该方法访问的任何静态都将被初始化,这意味着任何只读静态不仅在执行tier-3代码时已经初始化,而且它们的值永远不会改变.这反过来意味着,任何原始类型的只读静态(如bool、int等)都可以被视为常量,而不是静态只读字段,并且在第1层编译期间,JIT可以优化它们,就像优化常量一样.例如,在将DOTNET_JitDisasm设置为Program:Test后,尝试运行以下简单程序:

using System.Runtime.CompilerServices;

class Program
{
    static readonly bool Is64Bit = Environment.Is64BitProcess;

    static int Main()
    {
        int count = 0;
        for (int i = 0; i < 1_000_000_000; i++)
            if (Test())
                count++;
        return count;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static bool Test() => Is64Bit;
}           

当我这样做时,我得到以下输出:

// Assembly listing for method Program:Test():bool
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-0 compilation
// MinOpts code
// rbp based frame
// partially interruptible

G_M000_IG01:                ;; offset=0000H
       55                   push     rbp
       4883EC20             sub      rsp, 32
       488D6C2420           lea      rbp, [rsp+20H]

G_M000_IG02:                ;; offset=000AH
       48B9B8639A3FFC7F0000 mov      rcx, 0x7FFC3F9A63B8
       BA01000000           mov      edx, 1
       E8C220B25F           call     CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
       0FB60545580C00       movzx    rax, byte  ptr [(reloc 0x7ffc3f9a63ea)]

G_M000_IG03:                ;; offset=0025H
       4883C420             add      rsp, 32
       5D                   pop      rbp
       C3                   ret

// Total bytes of code 43

// Assembly listing for method Program:Test():bool
// 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

G_M000_IG02:                ;; offset=0000H
       B801000000           mov      eax, 1

G_M000_IG03:                ;; offset=0005H
       C3                   ret

// Total bytes of code 6           

注意,我们再次看到程序的两个输出:Test方法的汇编代码.首先,我们看到“Tier-0”代码,它正在访问静态(注意调用CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE指令).但是,我们看到了“Tier-1”代码,其中所有的开销都消失了,取而代之的是mov eax,1.由于必须执行“Tier-0”代码才能分层,“Tier-2”代码是在知道静态只读bool IS64位字段的值为真的情况下生成的(1),因此,整个方法是将值1存储到用于返回值的eax寄存器中.

这非常有用,现在编写组件时都考虑到了分层.考虑一下新的Regex源代码生成器,这将在后面的文章中讨论(Roslyn源代码生成器是几年前引入的;就像Roslyn分析器能够插入编译器并基于编译器从源代码中学习到的所有数据提供额外的诊断一样,Roslyn源代码生成器能够分析相同的数据,然后用额外的源代码进一步增加编译单元).Regex源生成器应用基于此的dotnet/runtime#67775技术.Regex支持设置流程范围的超时,该超时将应用于没有显式设置超时的Regex实例.这意味着,即使设置这样一个进程范围的超时非常罕见,Regex源生成器仍然需要输出与超时相关的代码,以备需要.它通过输出一些helper来实现,像这样:

static class Utilities
{
    internal static readonly TimeSpan s_defaultTimeout = AppContext.GetData("REGEX_DEFAULT_MATCH_TIMEOUT") is TimeSpan timeout ? timeout : Timeout.InfiniteTimeSpan;
    internal static readonly bool s_hasTimeout = s_defaultTimeout != Timeout.InfiniteTimeSpan;
}           

然后调用的方式,如下所示:

if (Utilities.s_hasTimeout)
{
    base.CheckTimeout();
}           

在第0层中,这些检查仍然会在程序集代码中发出,但在吞吐量很重要的第1层中,如果没有设置相关的AppContext开关,那么s_defaultTimeout将会是Timeout.infinittimeespan,此时s_hasTimeout将为false.由于s_hasTimeout是一个静态的只读bool, JIT将能够将其视为const,并且所有的条件,如if (Utilities.s_hasTimeout)将被视为与if (false)相等,并从汇编代码中完全清除为死代码.

但是,这有点老生常谈了.自从.NET Core 3.0引入分层编译以来,JIT就能够进行这样的优化.不过,现在在.NET 7中,在OSR中,它也可以在默认情况下对带有循环的方法进行优化(因此启用了类似于正则表达式的情况).然而,当OSR与另一个令人兴奋的特性:动态PGO相结合时,OSR的真正魔力开始发挥作用.

因JIT部分内容太多,这里进行拆分,PGO拆分为一篇博文.

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

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

继续阅读