You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何从C# JIT汇编代码判断是否创建了Action委托对象?

问题背景

假设我们有如下代码,我在待检查的代码位置添加了注释,运行环境为撰写本文时的最新.NET版本(.NET 5或6)。
我的目标是规避GC压力,不希望底层隐式创建Action对象。我可以接受少量垃圾内存,但无法接受高频调用函数时产生大量垃圾,进而引发不可接受的GC暂停,不过JIT或许足够智能,可以完全跳过对象创建流程。
为了验证这一点,我编写了如下程序,判断是否会生成new Action(...)的逻辑:

using System;

public class ClassWithMethod
{
    public void MethodToCall() 
    {
        Console.WriteLine("hi");
    }
}

public class Program
{
    private ClassWithMethod instance = new ClassWithMethod();
    
    private void Call(Action action)
    {
        action();
    }
    
    public void DoStuff()
    {
        Call(instance.MethodToCall);  // <=== 待检查的代码行
    }
    
    public static void Main()
    {
        Program program = new Program();
        program.DoStuff();
    }
}

该程序在amd64架构的Core CLR 5.0.721.25508环境下生成的汇编代码如下:

; Core CLR 5.0.721.25508 on amd64

ClassWithMethod..ctor()
    L0000: ret

ClassWithMethod.MethodToCall()
    L0000: mov rcx, 0x2d94b437fc8
    L000a: mov rcx, [rcx]
    L000d: jmp System.Console.WriteLine(System.String)

Program..ctor()
    L0000: push rsi
    L0001: sub rsp, 0x20
    L0005: mov rsi, rcx
    L0008: mov rcx, 0x7ffaf463cd50
    L0012: call 0x00007ffb4be79b70
    L0017: lea rcx, [rsi+8]
    L001b: mov rdx, rax
    L001e: call 0x00007ffb4be79780
    L0023: nop
    L0024: add rsp, 0x20
    L0028: pop rsi
    L0029: ret

Program.Call(System.Action)
    L0000: mov rcx, [rdx+8]
    L0004: mov rax, [rdx+0x18]
    L0008: jmp rax

Program.DoStuff()
    L0000: push rdi
    L0001: push rsi
    L0002: sub rsp, 0x28
    L0006: mov rsi, rcx
    L0009: mov rcx, 0x7ffaec4c4288
    L0013: call 0x00007ffb4be79b70
    L0018: mov rdi, rax
    L001b: mov rdx, [rsi+8]
    L001f: test rdx, rdx
    L0022: je short L004c
    L0024: lea rcx, [rdi+8]
    L0028: call 0x00007ffb4be79780
    L002d: mov rcx, ClassWithMethod.MethodToCall()
    L0037: mov [rdi+0x18], rcx
    L003b: mov rcx, [rdi+8]
    L003f: mov rax, [rdi+0x18]
    L0043: add rsp, 0x28
    L0047: pop rsi
    L0048: pop rdi
    L0049: jmp rax
    L004c: call System.MulticastDelegate.ThrowNullThisInDelegateToInstance()
    L0051: int3

Program.Main()
    L0000: push rsi
    L0001: sub rsp, 0x20
    L0005: mov rcx, 0x7ffaf463d0a0
    L000f: call 0x00007ffb4be79b70
    L0014: mov rsi, rax
    L0017: mov rcx, 0x7ffaf463cd50
    L0021: call 0x00007ffb4be79b70
    L0026: lea rcx, [rsi+8]
    L002a: mov rdx, rax
    L002d: call 0x00007ffb4be79780
    L0032: mov rcx, rsi
    L0035: add rsp, 0x20
    L0039: pop rsi
    L003a: jmp Program.DoStuff()

IL查看器输出的DoStuff方法IL代码如下:

.method public hidebysig 
    instance void DoStuff () cil managed 
{
    // Method begins at RVA 0x206c
    // Code size 24 (0x18)
    .maxstack 8

    IL_0000: ldarg.0
    IL_0001: ldarg.0
    IL_0002: ldfld class ClassWithMethod Program::'instance'
    IL_0007: ldftn instance void ClassWithMethod::MethodToCall()
    IL_000d: newobj instance void [System.Private.CoreLib]System.Action::.ctor(object, native int)
    IL_0012: call instance void Program::Call(class [System.Private.CoreLib]System.Action)
    IL_0017: ret
} // end of method Program::DoStuff

上面代码中的这一行:

IL_000d: newobj instance void [System.Private.CoreLib]System.Action::.ctor(object, native int)

似乎意味着会创建新的Action对象,这对我的场景来说非常不利。
但我无法判断JIT编译后的版本中是否真的发生了对象创建,汇编代码中存在如下行:

L0028: call 0x00007ffb4be79780

我不确定该指令是否调用的是构造函数,也不确定JIT是否省略了对象创建直接调用目标方法,请问如何从这段JIT汇编判断是否创建了对象?


解答

从你提供的JIT汇编可以明确每次调用DoStuff方法都会创建新的Action对象,判断依据如下:

  1. 先识别两个关键调用的功能:
  • L0013: call 0x00007ffb4be79b70:这是CoreCLR的托管堆内存分配函数,传入的rcx寄存器值0x7ffaec4c4288是Action类型的EEType指针,调用完成后rax寄存器返回新分配的Action对象地址,并存入rdi寄存器,这一步已经完成了Action对象的内存分配。
  • L0028: call 0x00007ffb4be79780:这就是Action的构造函数,入参rcx是刚分配的Action对象的_target字段地址(对应lea rcx, [rdi+8]),入参rdx是绑定的ClassWithMethod实例,完全对应IL中newobj指令的两个入参,后续代码还把方法指针写入了Action对象的_methodPtr字段(偏移0x18的位置),完整执行了委托初始化的全部流程。
  1. 虽然JIT把Call方法做了内联,最后直接跳转到MethodToCall执行,没有走常规的委托Invoke流程,但内存分配和构造的步骤完全没有被优化省略,高频调用DoStuff一定会在托管堆上生成大量Action对象,产生预期外的GC压力。

如果要完全避免委托分配,你可以选择以下优化方案:

  • 缓存Action对象:在类中预创建并缓存绑定好实例的Action,后续调用直接复用缓存的对象,仅会产生一次固定分配。
  • 用泛型+接口替代委托:定义无分配的调用约束,比如定义ICallable接口让ClassWithMethod实现,再把Call方法改成泛型方法Call<T>(T obj) where T : ICallable,JIT泛型特化后不会产生任何额外分配。
  • .NET 6及以上版本可以用函数指针(delegate*)替代Action传递,完全规避委托对象的创建。

内容的提问来源于stack exchange,提问作者Water

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 10:39:04