C# closure heap allocation happening at start of method

蹲街弑〆低调 提交于 2019-12-10 19:11:53

问题


I seem to have run into some odd behavior of the C# compiler.

Consider the following code sample:

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

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

Running this (in release) shows some unexpected allocations happening. Examining the IL shows that that the heap allocation triggered by the closure appears at the very start of the function, rather than inside the condition:

  .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

Am I missing something here, does anyone has an explanation for this strange behavior? Is it possible that Roslyn generates code which allocates for closures regardless of whether we actually execute them?


回答1:


This behavior is by design.

When your method has a closure, all variables used inside the closure must be part of the closure class (so that the lambda can access their current values).

Had the compiler not allocated the closure immediately, it would have to copy the values from local variables to fields on the closure class when the closure instance is created, wasting time and memory.

That would also make the codegen much riskier and more complicated if multiple lambdas with different reachabilities (or, worse, nested scopes) close over the same variables.




回答2:


As stated by SLacks, this behavior is by design, since x is a parameter to the function.

However, the allocation can be "moved into" the condition as follows:

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

In this specific scenario, the transformation is safe because x is not modified within the body of Foo, nor in the lambda. Also, the if statement is not executed within a loop (in which case the transformation might actually increase the number of allocations). The compiler does not make that analysis for you, but you can.



来源:https://stackoverflow.com/questions/41507166/c-sharp-closure-heap-allocation-happening-at-start-of-method

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!