【问题标题】:C# closure heap allocation happening at start of methodC#闭包堆分配发生在方法的开始
【发布时间】:2017-01-06 13:49:39
【问题描述】:

我似乎遇到了 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 是否有可能生成为闭包分配的代码,而不管我们是否实际执行它们?

【问题讨论】:

  • “发生了一些意外的分配” - 也可能意味着预期错误。

标签: c# .net closures roslyn


【解决方案1】:

此行为是设计使然。

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

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

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

【讨论】:

  • 闭包显然需要关闭他们使用的变量——我理解这部分。我不明白的是在方法的最开始发生了什么操作,不能推迟到以后,当方法实际使用时......
  • 在方法开头的代码中似乎也复制了局部变量值,所以我不明白您在第二句话中指的是什么附加副本。
  • 将值从本地复制到闭包类不仅代价高昂,而且在语义上也不正确。闭包的想法是您关闭变量,而不是值,因此如果您复制了值,则不会观察到对闭包外部的局部变量的更改,也不会观察到更改在关闭中制作。
  • @ShayRojansky:不;局部变量只存在于闭包中,所以没有什么可复制的(参数除外)。
  • 而且,Servy 是完全正确的;如果您在if 之后修改x,代码生成器需要检查闭包是否存在才能确定要写入的内容,这太可怕了。
【解决方案2】:

正如 Slack 所说,这种行为是设计使然,因为 x 是函数的参数。

但是,分配可以“移入”如下条件:

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

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

【讨论】:

  • 谢谢,是的,这就是我最终写东西的方式。不过,我很惊讶编译器没有为我这样做,并且很想知道是否有原因阻止它这样做。毕竟,它正在执行词法分析以了解变量 defined 的最高词法范围(在我的情况下是方法体,因为 x 是一个参数),它还可以检测到的最高词法范围变量被使用并在那里分配。
  • 这种答案不是很有帮助。我要问的是(显然)是否有充分的理由不实施此优化,这将在某些情况下减少堆分配而不会立即出现明显的缺点。可能是此功能的复杂性不能保证性能提升,或者是我没有考虑过的其他问题(这就是我询问社区的原因)。如果它不存在的唯一原因是没有人这样做,那么它可能会成为一个很好的 Roslyn 问题。
  • 你为什么不实施它,并衡量现实世界程序的性能提升?我相信您会发现 1) 功能比您想象的分析、开发和测试要复杂得多; 2)现实世界程序中的好处比你想象的要少。好处如此之低的原因之一是很容易自己进行这种优化,而不会显着降低代码的可读性。很多事情使它变得复杂。例如,如果这不是 if 语句,而是 while 循环(或循环中的 if),那么相同的转换可能会使性能变差。
  • 我为什么不自己实现呢?因为在进入一个潜在的大事情之前,我倾向于先在这里询问专家的意见。这就是这个问题的意义所在。您的第一点“该功能比您想象的更复杂”没有提供任何其他信息,我从未说过我认为这很微不足道。
  • 您对循环的评论是我一直在寻找的答案,谢谢...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-05-10
  • 1970-01-01
  • 2021-05-18
  • 2012-12-23
  • 1970-01-01
  • 1970-01-01
  • 2020-12-19
相关资源
最近更新 更多