让 .NET 11 的泛型虚方法变得更快!

泛型方法

.NET 中的泛型方法带有一个或多个类型参数,可以在保证类型安全的同时处理不同的数据类型。当我们希望代码针对具体类型进行特化,却又不想为每种类型重复编写一份代码时,泛型方法就很有用。

来看一个将两个值相加的泛型方法:

csharp 复制代码
T Add<T>(T a, T b) where T : IAdditionOperators<T, T, T>
{
    return a + b;
}

这个方法可以使用 intfloat,以及任何实现了 IAdditionOperators<T, T, T> 的类型来调用。

这种灵活性不仅强大,而且高效。对于值类型实例化,JIT 通常会生成针对确切类型特化的代码,因此可以直接使用底层运算,不需要装箱,也不需要运行时类型检查。为 intfloat 生成的代码也非常精简:

nasm 复制代码
Add[int](int,int):int:this (FullOpts):
       lea      eax, [rsi+rdx]
       ret

Add[float](float,float):float:this (FullOpts):
       vaddss   xmm0, xmm0, xmm1
       ret

可以看到,int 实例化变成了整数加法,float 实例化则变成了标量浮点加法。两个方法中都没有留下任何泛型分派。

如果调用方以具体类型调用这个方法,JIT 还可以更进一步,将调用内联。假设我们用常量调用这两个实例化:

csharp 复制代码
void Test()
{
    Console.WriteLine(Add(4, 5));
    Console.WriteLine(Add(4.0f, 5.0f));
}

JIT 可以内联这两次调用,再对两次加法进行常量折叠。最终生成的代码只剩下两次对 Console.WriteLine 的调用:

nasm 复制代码
Test():this (FullOpts):
       push     rax
       mov      edi, 9
       call     [System.Console:WriteLine(int)]
       vmovss   xmm0, dword ptr [reloc @RWD00]
       add      rsp, 8
       tail.jmp [System.Console:WriteLine(float)]
RWD00  dd       41100000h               ; 9

可以看到,两次 Add 调用都被内联了,结果被直接计算出来并传给 Console.WriteLine。一旦确定了确切的泛型方法,这层抽象就会从生成代码中消失。

虚方法

.NET 中的虚方法提供了多态行为。基类可以声明一个虚方法:

csharp 复制代码
class Base
{
    public virtual void Display()
    {
        Console.WriteLine("Base Display");
    }
}

接着创建一个派生类来重写它:

csharp 复制代码
class Derived : Base
{
    public override void Display()
    {
        Console.WriteLine("Derived Display");
    }
}

当我们在 Derived 实例上调用 Display 时,执行的是 Derived 中重写后的方法:

csharp 复制代码
void Test()
{
    Base obj = new Derived();
    obj.Display(); // Output: Derived Display
}

通常,每次虚调用都需要根据对象的实际运行时类型选择实现。这种分派比直接调用更昂贵。

不过,一种叫作去虚拟化(devirtualization)的优化可以消除这部分开销。在上面的例子中,JIT 能看出 obj 指向一个刚创建的 Derived,因此可以把虚调用目标替换为 Derived.Display。目标确定后,JIT 还可以继续内联这个调用。内联之后,对象分配也不再有用,于是同样会被消除:

nasm 复制代码
Program:Test():this (FullOpts):
       push     rax
       mov      rdi, 0x7E62ECE02D58      ; 'Derived Display'
       add      rsp, 8
       tail.jmp [System.Console:WriteLine(System.String)]

JIT 看出 obj 的实际类型是 Derived,于是把虚调用替换为对 Derived.Display 的直接调用,随后将其内联。最终代码直接以 "Derived Display" 为参数调用 Console.WriteLine。经过优化后,对象分配和虚调用都不复存在。

去虚拟化的意义不只是消除分派开销。得到直接调用目标后,内联器就能看到被调用方法的内容,往往可以在调用方中发现更大的优化空间。

JIT 可以在很多场景下对虚方法调用进行去虚拟化来提升性能,而且随着 .NET 不断发布新版本,能够覆盖的场景也越来越多。

泛型虚方法

下面来看看泛型虚方法(GVM)。

方法可以是泛型的,也可以是虚的。那么把两者结合起来会怎样?泛型虚方法带有类型参数,但具体实现仍然要根据接收者的运行时类型来选择。

例如,可以在接口中声明一个泛型方法:

csharp 复制代码
interface ICalculator
{
    T Calc<T>(T a, T b) where T : IAdditionOperators<T, T, T>;
}

然后由一个类提供实现:

csharp 复制代码
class AddCalculator : ICalculator
{
    public T Calc<T>(T a, T b) where T : IAdditionOperators<T, T, T>
    {
        return a + b;
    }
}

你可能会觉得,生成的代码应该与前面那个非虚泛型方法差不多。让我们看看通过接口调用时会发生什么。接收者显然是 AddCalculator,两次调用的方法类型参数也都是具体的:

csharp 复制代码
void Test()
{
    ICalculator calc = new AddCalculator();
    Console.WriteLine(calc.Calc(4, 5));
    Console.WriteLine(calc.Calc(4.0f, 5.0f));
}

.NET 10 生成的代码却是另一回事:

nasm 复制代码
Program:Test():this (FullOpts):
       push     rbp
       push     rbx
       push     rax
       lea      rbp, [rsp+0x10]
       mov      rdi, 0x7A7265C4B8D0      ; AddCalculator
       call     CORINFO_HELP_NEWSFAST
       mov      rbx, rax
       mov      rdi, rbx
       mov      rsi, 0x7A7265C45A80      ; ICalculator
       mov      rdx, 0x7A7265D6A6C8      ; token handle
       call     [CORINFO_HELP_VIRTUAL_FUNC_PTR]
       mov      rdi, rbx
       mov      esi, 4
       mov      edx, 5
       call     rax
       mov      edi, eax
       call     [System.Console:WriteLine(int)]
       mov      rdi, rbx
       mov      rsi, 0x7A7265C45A80      ; ICalculator
       mov      rdx, 0x7A7265D6A8A0      ; token handle
       call     [CORINFO_HELP_VIRTUAL_FUNC_PTR]
       vmovss   xmm1, dword ptr [reloc @RWD00]
       vmovss   xmm0, dword ptr [reloc @RWD04]
       mov      rdi, rbx
       call     rax
       nop
       add      rsp, 8
       pop      rbx
       pop      rbp
       tail.jmp [System.Console:WriteLine(float)]
RWD00  dd       40A00000h               ; 5
RWD04  dd       40800000h               ; 4

天哪,只是计算两个数的和,竟然生成了这么大一段代码!这里到底发生了什么?

首先,CORINFO_HELP_NEWSFAST 分配了 AddCalculator 对象。两次调用都需要这个对象,因此 JIT 将得到的对象引用保存在 rbx 中。

每次调用时,生成代码都会向 CORINFO_HELP_VIRTUAL_FUNC_PTR 传入三项信息:接收者、ICalculator 接口,以及一个用于标识所请求泛型方法实例化的令牌。该辅助函数解析出正确的实现,并将其地址返回到 rax 中。

随后,程序通过 call rax 调用这个地址。这是一次间接调用。由于 Calc<int>Calc<float> 是不同的 GVM 目标,第二个实例化还要再执行一次相同的解析过程。

辅助函数调用和间接调用只是看得见的成本。更大的损失在于,JIT 始终得不到 Calc<T> 的直接调用目标,因此无法内联任何一个实现。这样一来,前面那个泛型示例中将结果直接折叠为 9 的常量折叠也无法进行。

你可能会说,不如把 AddCalculator 从类改成结构体,看看是否会有帮助。泛型代码经常能在不装箱的情况下调用结构体的接口成员,也许改成结构体就能降低分派成本:

csharp 复制代码
struct AddCalculator : ICalculator
{
    public T Calc<T>(T a, T b) where T : IAdditionOperators<T, T, T>
    {
        return a + b;
    }
}

.NET 10 中生成的代码变成了:

nasm 复制代码
Test():this (FullOpts):
       push     rbp
       push     rbx
       push     rax
       lea      rbp, [rsp+0x10]
       mov      rdi, 0x77030962B8B8      ; AddCalculator
       call     CORINFO_HELP_NEWSFAST
       mov      rbx, rax
       mov      byte  ptr [rbx+0x08], 0
       mov      rdi, rbx
       mov      rsi, 0x770309625A80      ; ICalculator
       mov      rdx, 0x77030974A6C8      ; token handle
       call     [CORINFO_HELP_VIRTUAL_FUNC_PTR]
       mov      rdi, rbx
       mov      esi, 4
       mov      edx, 5
       call     rax
       mov      edi, eax
       call     [System.Console:WriteLine(int)]
       mov      rdi, rbx
       mov      rsi, 0x770309625A80      ; ICalculator
       mov      rdx, 0x77030974A8A0      ; token handle
       call     [CORINFO_HELP_VIRTUAL_FUNC_PTR]
       vmovss   xmm1, dword ptr [reloc @RWD00]
       vmovss   xmm0, dword ptr [reloc @RWD04]
       mov      rdi, rbx
       call     rax
       nop
       add      rsp, 8
       pop      rbx
       pop      rbp
       tail.jmp [System.Console:WriteLine(float)]
RWD00  dd       40A00000h               ; 5
RWD04  dd       40800000h               ; 4

结果反而更糟了!生成代码与类实现几乎完全相同,但这个版本还多了装箱。局部变量仍然是接口类型 ICalculator,所以将结构体赋给它时会发生装箱。CORINFO_HELP_NEWSFAST 分配装箱对象,紧随其后的存储指令初始化其中的值。之后的两次 GVM 调用仍然与之前一样,要经过辅助函数解析和间接分派。

这并不意味着对结构体的接口调用总会装箱。通过泛型类型参数进行受约束调用时,可以保留值类型并避免装箱。在这个例子中,值在两次调用发生之前就被转换成了 ICalculator,从而失去了这个优化机会。

普通虚调用主要取决于接收者类型和虚方法槽。GVM 调用还多了一个维度:方法实例化。即使两次调用使用相同的接收者和接口方法,解析 Calc<int> 与解析 Calc<float> 也可能得到不同的入口点。

.NET 运行时已经知道如何在运行时解析这种组合,上面生成代码中的 CORINFO_HELP_VIRTUAL_FUNC_PTR 做的正是这件事。JIT 优化真正困难的地方,是在编译期间证明某个接收者、接口方法和泛型实例化的组合只会指向一个确切目标。

如果 JIT 能确定这个目标,整条调用链就可以被逐步消除:

  1. 不再需要运行时 GVM 查找。
  2. 间接调用变成直接调用。
  3. 直接调用可以被内联,从而让方法体接受进一步优化。

然而在 .NET 10 中,第一步并不会发生,后续优化也因此全部被阻断。虽然 JIT 知道接收者是 AddCalculator,却无法根据这项信息解析出确切的 GVM 目标。生成代码仍然需要通过辅助函数查找并进行间接调用,内联器也就没有机会看到方法体。

泛型虚方法调用的去虚拟化

现在我们已经理解了问题,接下来就在 .NET 11 中着手解决它。

GVM 调用可能有多种不同的形态:

  1. 接收者可以是类,也可以是结构体。
  2. 实例化可以使用值类型,也可以使用引用类型。
  3. 确切的泛型上下文可能在编译时已知,也可能需要运行时查找。

第一个维度很好理解。结构体被装箱并通过接口调用时,分派目标可能是一段称为拆箱存根(unboxing stub)的特殊代码。它会调整装箱对象的 this 指针,使其指向结构体数据,然后把控制转交给真正的方法体。如果 JIT 对调用进行了去虚拟化,就可以直接以拆箱后的入口点为目标,并且有机会一并消除装箱。

第二个维度来自泛型方法既能以值类型实例化,也能以引用类型实例化。JIT 通常会为值类型特化代码,而引用类型实例化通常共享同一份本机代码。

这叫作共享泛型代码。当类型参数是引用类型时,JIT 通常只生成一份方法体,并让所有引用类型实例化共享它。

共享实例化在内部使用一种名为 System.__Canon 的特殊类型表示。由于本机代码是共享的,被调用方有时需要一个隐藏的泛型上下文参数来标识确切实例化。方法体可以利用这个上下文执行与具体类型有关的操作。

例如,来看下面这些共享泛型方法调用:

csharp 复制代码
void Test()
{
    Console.WriteLine(GetType<string>());
    Console.WriteLine(GetType<object>());
}

[MethodImpl(MethodImplOptions.NoInlining)]
Type GetType<T>()
{
    return typeof(T);
}

所有引用类型只会共享一份 GetType<T>,JIT 会为每个确切实例化传入隐藏的泛型上下文参数。生成代码如下:

nasm 复制代码
Test():this (FullOpts):
       push     rbp
       push     rbx
       push     rax
       lea      rbp, [rsp+0x10]
       mov      rbx, rdi
       mov      rdi, rbx
       mov      rsi, 0x7F694233AAC8      ; Program:GetType[System.String]():System.Type:this
       call     [Program:GetType[System.__Canon]():System.Type:this]
       mov      rdi, rax
       call     [System.Console:WriteLine(System.Object)]
       mov      rdi, rbx
       mov      rsi, 0x7F694233AB50      ; Program:GetType[System.Object]():System.Type:this
       call     [Program:GetType[System.__Canon]():System.Type:this]
       mov      rdi, rax
       add      rsp, 8
       pop      rbx
       pop      rbp
       tail.jmp [System.Console:WriteLine(System.Object)]

GetType[System.__Canon]():System.Type:this (FullOpts):
       push     rax
       mov      qword ptr [rsp], rsi
       mov      rdi, qword ptr [rsi+0x18]
       mov      rdi, qword ptr [rdi]
       call     CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE
       nop      
       add      rsp, 8
       ret      

可以看到,调用方通过 rsi 传入隐藏参数,用来标识 GetType<T> 的确切实例化。在这个例子中,该参数是方法上下文,共享方法体从中取出实际的类型参数。

这意味着,对 GVM 调用进行去虚拟化并不总是简单地更换调用目标。将 GVM 调用改为直接调用后,如果目标使用共享泛型代码,JIT 还必须提供正确的隐藏上下文参数。

再来看第三个维度。在上面的例子中,编译调用方时类型参数已经确定,因此 JIT 可以直接生成对 GetType<string>GetType<object> 的调用。但如果确切上下文只有在运行时才能获得,JIT 就必须动态查找所需目标或上下文。在内部,这叫作运行时查找。

.NET 11 中的改进

泛型虚方法去虚拟化

我在这个问题上投入了将近一年时间,目前已经取得了很大进展。相关的主要工作内容都汇总在这个 issue 里:JIT: Devirtualization for generic virtual methods · Issue #112596 · dotnet/runtime

还记得上一节的三个维度吗?在 .NET 11 中,我们改进了前两个维度。

现在,无论接收者是类还是结构体,也无论方法使用值类型还是引用类型实例化,JIT 都可以对 GVM 调用进行去虚拟化。第三个维度,也就是需要运行时查找的情况,仍在开发中,计划在 .NET 12 中完成。

现在看看同一个例子在 .NET 11 中生成的代码。

nasm 复制代码
TestClass():this (FullOpts):
       push     rax
       mov      edi, 9
       call     [System.Console:WriteLine(int)]
       vmovss   xmm0, dword ptr [reloc @RWD00]
       add      rsp, 8
       tail.jmp [System.Console:WriteLine(float)]
RWD00  	dd	41100000h		;         9

TestStruct():this (FullOpts):
       push     rax
       mov      edi, 9
       call     [System.Console:WriteLine(int)]
       vmovss   xmm0, dword ptr [reloc @RWD00]
       add      rsp, 8
       tail.jmp [System.Console:WriteLine(float)]
RWD00  	dd	41100000h		;         9

TestClassTestStruct 分别在类和结构体上调用 Calc。可以看到,生成代码已经达到最优。GVM 调用被去虚拟化,结构体装箱也被消除。JIT 可以内联 Calc 并折叠两次加法,最终将值 9 直接传给 Console.WriteLine

方法使用引用类型实例化时,这项优化同样有效。string 不满足 IAdditionOperators<T, T, T> 约束,所以我们换一个例子。

假设有一个泛型虚方法,可以把传入的字符串解析为目标类型:

csharp 复制代码
void Test()
{
    ICanParse p1 = new ClassParser();
    Console.WriteLine(p1.Parse<string>("test"));
    ICanParse p2 = new StructParser();
    Console.WriteLine(p2.Parse<string>("test"));
}

interface ICanParse
{
    T Parse<T>(string s) where T : IParsable<T>;
}

class ClassParser : ICanParse
{
    public T Parse<T>(string s) where T : IParsable<T>
    {
        return T.Parse(s, null);
    }
}

struct StructParser : ICanParse
{
    public T Parse<T>(string s) where T : IParsable<T>
    {
        return T.Parse(s, null);
    }
}

.NET 10 为 Test 生成的代码如下:

nasm 复制代码
Test():this (FullOpts):
       push     rbp
       push     r15
       push     r14
       push     rbx
       push     rax
       lea      rbp, [rsp+0x20]
       mov      rdi, 0x79D16F05B8D0      ; ClassParser
       call     CORINFO_HELP_NEWSFAST
       mov      rbx, rax
       mov      rdi, rbx
       mov      r15, 0x79D16F05B800      ; ICanParse
       mov      rsi, r15
       mov      rdx, 0x79D16F160A10      ; token handle
       call     [CORINFO_HELP_VIRTUAL_FUNC_PTR]
       mov      rdi, rbx
       mov      rbx, 0x79D16C202680      ; 'test'
       mov      rsi, rbx
       call     rax
       mov      rdi, rax
       call     [System.Console:WriteLine(System.String)]
       mov      rdi, 0x79D16F05B9B8      ; StructParser
       call     CORINFO_HELP_NEWSFAST
       mov      r14, rax
       mov      byte  ptr [r14+0x08], 0
       mov      rdi, r14
       mov      rsi, r15
       mov      rdx, 0x79D16F160A10      ; token handle
       call     [CORINFO_HELP_VIRTUAL_FUNC_PTR]
       mov      rdi, r14
       mov      rsi, rbx
       call     rax
       mov      rdi, rax
       add      rsp, 8
       pop      rbx
       pop      r14
       pop      r15
       pop      rbp
       tail.jmp [System.Console:WriteLine(System.String)]

可以看到,运行时解析和间接调用仍然存在,StructParser 也会在接口调用前被装箱。

.NET 11 为 Test 生成的代码变成了:

nasm 复制代码
Test():this (FullOpts):
       push     rbx
       mov      rbx, 0x70FBB5202A70      ; 'test'
       mov      rdi, rbx
       call     [System.Console:WriteLine(System.String)]
       mov      rdi, rbx
       pop      rbx
       tail.jmp [System.Console:WriteLine(System.String)]

现在的代码已经达到最优,两次调用最终都把字符串字面量直接传给了 Console.WriteLine

泛型上下文中的默认接口方法去虚拟化

这项工作也适用于在泛型接口中声明的默认接口方法(DIM)。来看下面的代码:

csharp 复制代码
void Test()
{
    IGenericA<string> a = new AImpl();
    a.B("GVM");
    a.C("Generic DIM");
}

interface IA
{
    void A(object o);
    void B<T>(T o);
}

interface IGenericA<T> : IA
{
    void C(T o) => B(o);
}

class AImpl : IGenericA<string>
{
    public void A(object o)
    {
        Console.WriteLine(o);
    }

    public void B<T>(T o)
    {
        Console.WriteLine(o);
    }
}

.NET 10 生成的代码是:

nasm 复制代码
Test():this (FullOpts):
       push     rbx
       sub      rsp, 32
       mov      rcx, 0x7FFB383AF518
       call     CORINFO_HELP_NEWSFAST
       mov      rbx, rax
       mov      rcx, rbx
       mov      rdx, 0x7FFB383AF348
       mov      r8, 0x7FFB383AF828
       call     CORINFO_HELP_VIRTUAL_FUNC_PTR
       mov      rcx, rbx
       mov      rdx, 0xA025C36200
       call     rax
       mov      rcx, rbx
       mov      r11, 0x7FFB380E0070
       mov      rdx, 0xA025C36220
       call     [r11]IGenericA`1[System.__Canon]:C(System.__Canon):this
       nop      
       add      rsp, 32
       pop      rbx
       ret      

.NET 11 中则变成了:

nasm 复制代码
Test():this (FullOpts):
       sub      rsp, 40
       mov      rcx, 0xA0261EE6F0      ; 'GVM'
       call     [System.Console:WriteLine(System.Object)]
       mov      rcx, 0xA0261EE710      ; 'Generic DIM'
       call     [System.Console:WriteLine(System.Object)]
       nop
       add      rsp, 40
       ret

所有间接调用都消失了,生成代码达到了最优状态。

顺手优化一下 where T : new()

有多少人在泛型代码中用过 where T : new()

在带有 new() 约束的泛型方法中,new T() 会被编译成对 Activator.CreateInstance<T>() 的调用。我还为 .NET 11 贡献了一项改进,让 JIT 知道 Activator.CreateInstance<T>() 的结果具有确切类型 T。这样,JIT 就能对返回对象上的后续调用进行去虚拟化。

.NET 11 中的最终成果

我编写了一个基准测试用来比较 .NET 10 和 .NET 11 中 GVM 调用的性能与分配开销。测试代码和结果可以在 GitHub Gist 中查看。

下面是这个基准测试在 .NET 10 与 .NET 11 中生成代码的差异:

diff 复制代码
 G_M000_IG01:                ;; offset=0x0000
-       push     rdi
-       push     rsi
-       push     rbp
        push     rbx
-       sub      rsp, 40
+       sub      rsp, 32
  
-G_M000_IG02:                ;; offset=0x0008
-       mov      rcx, 0x7FFCFE6A32B8
-       call     CORINFO_HELP_NEWSFAST
-       mov      rbx, rax
-       mov      byte  ptr [rbx+0x08], 0
-       mov      rcx, 0x7FFCFE67EBE0
-       call     CORINFO_HELP_NEWSFAST
-       mov      rsi, rax
-       mov      rcx, rbx
-       mov      rdi, 0x7FFCFE6A2AC8
-       mov      rdx, rdi
-       mov      r8, 0x7FFCFE6A35C8
-       call     CORINFO_HELP_VIRTUAL_FUNC_PTR
-       mov      rcx, rbx
-       mov      rdx, rsi
-       call     rax
-       mov      rdx, rax
-       mov      rcx, rbx
-       mov      r11, 0x7FFCFE3B0100
-       call     [r11]Test.IFooConsumer`1[System.__Canon]:Consume(System.__Canon):this
-       mov      rsi, 0x7FFCFE6A3690
-       mov      rcx, rsi
-       call     CORINFO_HELP_NEWSFAST
-       mov      rbp, rax
-       mov      rcx, rbx
-       mov      rdx, rdi
-       mov      r8, 0x7FFCFE6A38F8
-       call     CORINFO_HELP_VIRTUAL_FUNC_PTR
-       mov      rcx, rbx
-       mov      rdx, rbp
-       call     rax
-       mov      rdx, rax
-       mov      rcx, rbx
-       mov      r11, 0x7FFCFE3B0108
-       call     [r11]Test.IFooConsumer`1[System.__Canon]:Consume(System.__Canon):this
-       mov      rcx, rsi
-       call     CORINFO_HELP_NEWSFAST
-       mov      rdx, rax
-       mov      rcx, rbx
-       mov      r11, 0x7FFCFE3B0110
-       call     [r11]Test.IFooConsumer`1[System.__Canon]:Consume(System.__Canon):this
-       mov      rcx, rsi
-       call     CORINFO_HELP_NEWSFAST
-       mov      rbp, rax
-       mov      rcx, rbx
-       mov      rdx, rdi
-       mov      r8, 0x7FFCFE6A38F8
-       call     CORINFO_HELP_VIRTUAL_FUNC_PTR
-       mov      rcx, rbx
-       mov      rdx, rbp
-       call     rax
-       mov      rdx, rax
-       mov      rcx, rbx
-       mov      r11, 0x7FFCFE3B0118
-       call     [r11]Test.IFooConsumer`1[System.__Canon]:Consume(System.__Canon):this
-       mov      rcx, rsi
-       call     CORINFO_HELP_NEWSFAST
-       mov      rdx, rax
-       mov      rcx, rbx
-       mov      r11, 0x7FFCFE3B0128
-       call     [r11]Test.IFooConsumer`1[System.__Canon]:Consume(System.__Canon):this
-       mov      rcx, rsi
-       call     CORINFO_HELP_NEWSFAST
-       mov      rdx, rax
-       mov      rcx, rbx
-       mov      r11, 0x7FFCFE3B0138
- 
-G_M000_IG03:                ;; offset=0x0138
-       call     [r11]Test.IFooConsumer`1[System.__Canon]:Consume(System.__Canon):this
+G_M000_IG02:                ;; offset=0x0005
+       mov      rcx, 0x7FFC55594270
+       call     [System.Activator:CreateInstance[System.__Canon]():System.__Canon]
+       cmp      byte  ptr [rax], al
+       mov      rcx, 0x7FFC555943B0
+       mov      rdx, 0x224E7636488
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rcx, 0x7FFC555943B0
+       mov      rdx, 0x224E7636488
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rcx, 0x7FFC55594530
+       call     [System.Activator:CreateInstance[System.__Canon]():System.__Canon]
+       cmp      byte  ptr [rax], al
+       mov      rcx, 0x7FFC555943B0
+       mov      rbx, 0x224E76364D0
+       mov      rdx, rbx
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rcx, 0x7FFC555943B0
+       mov      rdx, rbx
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rcx, 0x7FFC555943B0
+       mov      rdx, rbx
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rcx, 0x7FFC55594530
+       call     [System.Activator:CreateInstance[System.__Canon]():System.__Canon]
+       cmp      byte  ptr [rax], al
+       mov      rcx, 0x7FFC555943B0
+       mov      rdx, rbx
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rcx, 0x7FFC555943B0
+       mov      rdx, rbx
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rdx, 0x224E76364D0
+       mov      rcx, 0x7FFC555943B0
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
+       mov      rdx, 0x224E76364D0
+       mov      rcx, 0x7FFC555943B0
+       call     [Test.Program:Use[System.__Canon](System.__Canon)]
        nop
  
-G_M000_IG04:                ;; offset=0x013C
-       add      rsp, 40
+G_M000_IG03:                ;; offset=0x010D
+       add      rsp, 32
        pop      rbx
-       pop      rbp
-       pop      rsi
-       pop      rdi
-       ret

可以看到,间接虚调用和装箱都消失了。JIT 现在可以对 GVM 调用进行去虚拟化,内联方法体,并继续执行后续优化。

在我的机器上,.NET 10 大约耗时 6,972 毫秒,每次迭代分配 240 字节。.NET 11 运行相同的基准测试只需大约 2,498 毫秒,每次迭代也只分配 72 字节。

性能提升了 2.79 倍,分配量则降至原来的约三分之一。

以上改进现已包含在 .NET 11 Preview 7 中,并且不仅对 JIT 有效,对 R2R 和 NativeAOT 同样生效。而包括运行时查找场景在内的更多改进预计会在 .NET 12 中到来。

读者们不妨亲自试一试看看这些工作带来的变化!

相关推荐
宝桥南山3 小时前
Microsoft Agent Framework(.NET) - 尝试一下从MCP Server上获取Agent Skills
ai·微软·c#·aigc·.net·.netcore
少控科技5 小时前
农场设备管理代码(1)
开发语言·c#
海盗12349 小时前
微软技术日报·2026-08-12——Windows 11多通道累积更新推送十余项功能改进;.NET WebSocket DoS紧急修复
windows·microsoft·.net
椒颜皮皮虾྅20 小时前
OpenVINO C# API 3.3.1:支持OpenVINO 2026.3及GenAI增强
人工智能·c#·openvino
半亩码田1 天前
C#转Python第3.1篇:Python 的 class 没有访问修饰符?面向对象的另一条路
开发语言·python·c#
唐青枫1 天前
别再手写 args[0]:C#.NET CommandLineParser 命令行工具实战详解
c#·.net
LccKyI1 天前
C#学习day06(方法/函数及其关键字,附思维导图)
经验分享·学习·c#
.Peter1 天前
.NET/WPF 程序在部分 Windows 电脑无法保存用户环境变量:使用 DPAPI 实现安全兼容
windows·信息安全·c#·.net·wpf·环境变量·dpapi
农村小镇哥1 天前
怎么把HTM格式转化成WORD
ui·c#·word