.NET 7 中的性能改进(二)

原文 | Stephen Toub

翻译 | 郑子铭

堆栈替换 (On-Stack Replacement)

堆栈替换 (OSR) 是 .NET 7 中最酷的 JIT 功能之一。但要真正了解 OSR,我们首先需要了解分层编译 (tiered compilation),所以快速回顾一下…….

具有 JIT 编译器的托管环境必须处理的问题之一是启动和吞吐量之间的权衡。从历史上看,优化编译器的工作就是优化,以便在运行时实现应用程序或服务的最佳吞吐量。但是这种优化需要分析,需要时间,并且执行所有这些工作会导致启动时间增加,因为启动路径上的所有代码(例如,在 Web 服务器可以为第一个请求提供服务之前需要运行的所有代码)需要编译。因此 JIT 编译器需要做出权衡:以更长的启动时间为代价获得更好的吞吐量,或者以降低吞吐量为代价获得更好的启动时间。对于某些类型的应用程序和服务,权衡很容易,例如如果您的服务启动一次然后运行数天,那么启动时间多几秒并不重要,或者如果您是一个控制台应用程序,它将进行快速计算并退出,启动时间才是最重要的。但是 JIT 如何知道它处于哪种场景中,我们真的希望每个开发人员都必须了解这些类型的设置和权衡并相应地配置他们的每个应用程序吗?对此的一种解决方案是提前编译,它在 .NET 中采用了多种形式。例如,所有核心库都是“crossgen”的,这意味着它们已经通过生成前面提到的 R2R 格式的工具运行,生成包含汇编代码的二进制文件,只需稍作调整即可实际执行;并非每个方法都可以为其生成代码,但足以显着减少启动时间。当然,这种方法有其自身的缺点,例如JIT 编译器的承诺之一是它可以利用当前机器/进程的知识来进行最佳优化,例如,R2R 图像必须采用特定的基线指令集(例如,哪些向量化指令可用),而JIT 可以看到实际可用的东西并使用最好的。 “分层编译”提供了另一种答案,无论是否使用这些其他提前 (ahead-of-time) (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 倍(.NET 6 中的默认设置)。那是因为这个 main 函数永远不会对其应用优化。通过将 DOTNET_TC_QuickJitForLoops 设置为 1,我们说“JIT,请将分层也应用于带循环的方法”,但这种带循环的方法只会被调用一次,因此在整个过程中它最终保持在“层” -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]

强调 Main 确实被编译了两次。这怎么可能?堆栈替换。

栈上替换背后的想法是一种方法不仅可以在调用之间替换,甚至可以在它执行时替换,当它“在堆栈上”时。除了用于调用计数的第 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”)编译的方法的初始版本(“Tier-0”)。其次,注意汇编的这一行:

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

我们的 IsAsciiDigit 辅助方法很容易内联,但它没有被内联;相反,程序集调用了它,实际上我们可以在下面看到为 IsAsciiDigit 生成的代码(也称为“MinOpts”)。为什么?因为内联是一种优化(一个非常重要的优化),它作为第 0 层的一部分被禁用(因为做好内联的分析也非常昂贵)。第三,我们可以看到 JIT 输出的代码来检测这个方法。这有点复杂,但我会指出相关部分。首先,我们看到:

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

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

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 辅助方法。该助手负责触发优化方法的创建(如果尚不存在)、修复所有适当的跟踪状态并跳转到新方法。事实上,如果您再次查看运行该程序的控制台输出,您会看到 Main 方法的另一个输出:

; 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

所以我们知道这既是优化的“一级”代码,也是该方法的“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 进行比较。听起来很像我们的 IsAsciiDigit 实现 ((uint)(c - ' 0') <= 9),不是吗?那是因为它是。帮助程序已成功内联到这个现在优化的代码中。

太好了,现在在 .NET 7 中,我们可以在很大程度上避免启动和吞吐量之间的权衡,因为 OSR 支持分层编译以应用于所有方法,即使是那些长时间运行的方法。许多 PR 都致力于实现这一点,包括过去几年的许多 PR,但所有功能在发布时都被禁用了。感谢 dotnet/runtime#62831 等改进,它在 Arm64 上实现了对 OSR 的支持(以前只实现了 x64 支持),以及 dotnet/runtime#63406 和 dotnet/runtime#65609 修改了 OSR 导入和 epilogs 的处理方式,dotnet/runtime #65675 默认启用 OSR(并因此启用 DOTNET_TC_QuickJitForLoops)。

但是,分层编译和 OSR 不仅仅与启动有关(尽管它们在那里当然非常有价值)。它们还涉及进一步提高吞吐量。尽管分层编译最初被设想为一种在不损害吞吐量的情况下优化启动的方法,但它已经变得远不止于此。 JIT 可以在第 0 层期间了解有关方法的各种信息,然后将其用于第 1 层。例如,执行第 0 层代码这一事实意味着该方法访问的任何静态都将被初始化,这意味着任何只读静态不仅会在第 1 层代码执行时被初始化,而且它们的价值观永远不会改变。这反过来意味着原始类型(例如 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

请注意,我们再次看到 Program:Test 的两个输出。首先,我们看到“第 0 层”代码,它正在访问静态(注意调用 CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE 指令)。但随后我们看到“Tier-1”代码,其中所有开销都消失了,取而代之的是 mov eax, 1。由于必须执行“Tier-0”代码才能使其分层, “Tier-1”代码是在知道 static readonly bool Is64Bit 字段的值为 true (1) 的情况下生成的,因此该方法的全部内容是将值 1 存储到用于返回值的 eax 寄存器中。

这非常有用,以至于现在在编写组件时都考虑到了分层。考虑一下新的 Regex 源代码生成器,这将在本文后面讨论(Roslyn 源代码生成器是几年前推出的;就像 Roslyn 分析器如何能够插入编译器并根据编译器的所有数据进行额外的诊断一样从源代码中学习,Roslyn 源代码生成器能够分析相同的数据,然后使用额外的源进一步扩充编译单元)。正则表达式源生成器在 dotnet/runtime#67775 中应用了基于此的技术。 Regex 支持设置一个进程范围的超时,该超时应用于未明确设置超时的 Regex 实例。这意味着,即使设置这种进程范围的超时非常罕见,Regex 源代码生成器仍然需要输出与超时相关的代码,以备不时之需。它通过输出一些像这样的助手来做到这一点:

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.InfiniteTimeSpan,此时 s_hasTimeout 将为错误的。并且由于 s_hasTimeout 是一个静态只读布尔值,JIT 将能够将其视为一个常量,并且所有条件如 if (Utilities.s_hasTimeout) 将被视为等于 if (false) 并从汇编代码中完全消除为死代码。

但是,这有点旧闻了。自从 .NET Core 3.0 中引入分层编译以来,JIT 已经能够进行这样的优化。不过,现在在 .NET 7 中,有了 OSR,它也可以默认为带循环的方法这样做(从而启用像正则表达式这样的情况)。然而,OSR 的真正魔力在与另一个令人兴奋的功能结合使用时才会发挥作用:动态 PGO。

原文链接

Performance Improvements in .NET 7