C# 闭包堆分配发生在方法开始时

C# closure heap allocation happening at start of method

我似乎 运行 了解 C# 编译器的一些奇怪行为。

考虑以下代码示例:

static void Main(string[] args)
{
    Foo(false, 8);
}

public static void Foo(bool execute, int x)
{
    if (execute)
    {
        Task.Run(() => Console.WriteLine(x));
    }
}

运行 这(在发行版中)显示发生了一些意外的分配。检查 IL 表明由闭包触发的堆分配出现在函数的最开始,而不是在条件内:

  .method public hidebysig static void 
    Foo(
      bool execute, 
      int32 x
    ) cil managed 
  {
    .maxstack 2
    .locals init (
      [0] class Test.Program/'<>c__DisplayClass1_0' 'CS$<>8__locals0'
    )

    IL_0000: newobj       instance void Test.Program/'<>c__DisplayClass1_0'::.ctor()
    IL_0005: stloc.0      // 'CS$<>8__locals0'
    IL_0006: ldloc.0      // 'CS$<>8__locals0'
    IL_0007: ldarg.1      // x
    IL_0008: stfld        int32 Test.Program/'<>c__DisplayClass1_0'::x

    // [18 13 - 18 25]
    IL_000d: ldarg.0      // execute
    IL_000e: brfalse.s    IL_0022

    // [20 17 - 20 54]
    IL_0010: ldloc.0      // 'CS$<>8__locals0'
    IL_0011: ldftn        instance void Test.Program/'<>c__DisplayClass1_0'::'<Foo>b__0'()
    IL_0017: newobj       instance void [mscorlib]System.Action::.ctor(object, native int)
    IL_001c: call         class [mscorlib]System.Threading.Tasks.Task [mscorlib]System.Threading.Tasks.Task::Run(class [mscorlib]System.Action)
    IL_0021: pop          

    // [22 9 - 22 10]
    IL_0022: ret          

  } // end of method Program::Foo

我是不是遗漏了什么,有人对这种奇怪的行为有解释吗? Roslyn 是否有可能生成分配闭包的代码,而不管我们是否实际执行它们?

此行为是设计使然。

当您的方法有闭包时,闭包内使用的所有变量都必须是闭包的一部分 class(以便 lambda 可以访问它们的当前值)。

如果编译器没有立即分配闭包,它必须在创建闭包实例时将局部变量的值复制到闭包上的字段class,从而浪费时间和内存。

如果具有不同可达性(或更糟糕的是,嵌套作用域)的多个 lambda 关闭相同的变量,这也会使代码生成变得更加危险和复杂。

如 SLacks 所述,此行为是设计使然,因为 x 是函数的参数。

但是,分配可以"moved into"条件如下:

public static void Foo(bool execute, int x)
{
    if (execute)
    {
        int localx = x;
        Task.Run(() => Console.WriteLine(localx));
    }
}

在这种特定情况下,转换是安全的,因为 x 没有在 Foo 的主体内修改,也没有在 lambda 中修改。此外,if 语句不在循环内执行(在这种情况下,转换实际上可能会增加分配的数量)。编译器不会为您进行分析,但您可以。