为什么这个非常简单的C#方法会产生这种不合逻辑的CIL代码?

lpm*_*ell 16 .net c# cil ildasm ilasm

我最近一直在挖掘IL,我注意到了C#编译器的一些奇怪的行为.以下方法是一个非常简单且可验证的应用程序,它将立即退出,退出代码为1:

static int Main(string[] args)
{
    return 1;
}
Run Code Online (Sandbox Code Playgroud)

当我使用Visual Studio Community 2015编译它时,会生成以下IL代码(添加注释):

.method private hidebysig static int32 Main(string[] args) cil managed
{
  .entrypoint
  .maxstack  1
  .locals init ([0] int32 V_0)     // Local variable init
  IL_0000:  nop                    // Do nothing
  IL_0001:  ldc.i4.1               // Push '1' to stack
  IL_0002:  stloc.0                // Pop stack to local variable 0
  IL_0003:  br.s       IL_0005     // Jump to next instruction
  IL_0005:  ldloc.0                // Load local variable 0 onto stack
  IL_0006:  ret                    // Return
}
Run Code Online (Sandbox Code Playgroud)

如果我要手写这个方法,看起来使用以下IL可以获得相同的结果:

.method static int32 Main()
{
  .entrypoint
  ldc.i4.1               // Push '1' to stack
  ret                    // Return
}
Run Code Online (Sandbox Code Playgroud)

是否存在我不知道的潜在原因使这成为预期的行为?

或者只是组装好的IL对象代码进一步优化,所以C#编译器不必担心优化?

Jon*_*eet 23

您显示的输出是用于调试版本.使用发布版本(或基本上启用了优化),C#编译器会生成您手动编写的相同IL.

我强烈怀疑这一切都是为了使调试器的工作变得更容易,基本上 - 使其更容易中断,并在返回之前查看返回值.

道德:当你想运行优化的代码时,请确保你没有要求编译器生成旨在调试的代码:)

  • 乔恩的怀疑当然是正确的.@lpmitchell:你会在未经优化的代码中看到很多东西,那些可能是"短暂的"的值 - 也就是说,只是被推到评估堆栈然后在我们完成它们时弹出 - 而是被存储和读取来自特定的局部变量堆栈槽.这对数字1没有任何作用,但想象一下是否存在对象引用; 存储它并检索它使GC不太可能比你期望的更早地收集对象,这有助于调试. (6认同)
  • 这很有道理,谢谢!正如预期在发布模式下编译时,IL完全符合预期 (2认同)

Eri*_*ert 11

乔恩的回答当然是正确的; 这个答案是跟进这个评论:

@EricLippert本地很有意义,但有没有任何理由可以用于br.s指令,还是只是出于方便的发射器代码?我想如果编译器想在那里插入一个断点占位符,它可能只是发出一个nop ...

如果你看一个更复杂的程序片段,看似无意义的分支的原因变得更加明智:

public int M(bool b) {
    if (b) 
      return 1; 
    else 
      return 2;
}
Run Code Online (Sandbox Code Playgroud)

未经优化的IL是

    IL_0000: nop
    IL_0001: ldarg.1
    IL_0002: stloc.0
    IL_0003: ldloc.0
    IL_0004: brfalse.s IL_000a
    IL_0006: ldc.i4.1
    IL_0007: stloc.1
    IL_0008: br.s IL_000e
    IL_000a: ldc.i4.2
    IL_000b: stloc.1
    IL_000c: br.s IL_000e
    IL_000e: ldloc.1
    IL_000f: ret
Run Code Online (Sandbox Code Playgroud)

请注意,有两个return语句但只有一条ret指令.在未经优化的IL中,代码生成简单返回语句的模式是:

  • 填充您要返回堆栈槽的值
  • 分支/离开到方法的末尾
  • 在方法结束时,从插槽中读取值并返回

也就是说,未经优化的代码使用单点返回形式.

在这种情况下和原始海报所示的简单情况下,该模式导致产生"分支到下一个"情况.生成未经优化的代码时,"删除任何分支到下一个"优化器不会运行,因此它仍然存在.