17

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

static int Main(string[] args)
{
    return 1;
}

当我使用 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
}

如果我要手写这个方法,似乎可以使用以下 IL 实现相同的结果:

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

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

还是只是组装后的 IL 目标代码进一步优化,所以 C# 编译器不必担心优化?

4

3 回答 3

24

您显示的输出用于调试构建。通过发布版本(或基本上打开优化),C# 编译器生成与您手动编写的相同的 IL。

我强烈怀疑这一切都是为了让调试器的工作更容易,基本上 - 让它更容易破解,并在返回之前查看返回值。

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

于 2018-01-25T15:01:46.993 回答
12

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

@EricLippert 本地非常有意义,但是该 br.s 指令是否有任何理由,或者它只是为了方便发射器代码?我猜如果编译器想在那里插入一个断点占位符,它可以发出一个 nop ...

如果您查看一个更复杂的程序片段,那么看似毫无意义的分支的原因会变得更加合理:

public int M(bool b) {
    if (b) 
      return 1; 
    else 
      return 2;
}

未优化的 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

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

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

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

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

于 2018-01-25T19:54:16.443 回答
-4

我要写的不是真正的.NET 特定的,而是一般的,我不知道.NET 在生成CIL 时识别和使用的优化。语法树(以及语法解析器本身)识别具有以下词位的 return 语句:

returnStatement ::= RETURN expr ;

其中 returnStatement 和 expr 是非终结符,RETURN 是终结符(返回令牌),因此当访问节点以获取常量时1,解析器的行为就好像它是表达式的一部分。为了进一步说明我的意思,以下代码:

return 1 + 1;

对于使用表达式堆栈的(虚拟)机器来说,看起来像这样:

push const_1 // Pushes numerical value '1' to expression stack
push const_1 // Pushes numerical value '1' to expression stack
add          // result = pop() + pop(); push(result)
return       // pops the value on the top of the stack and returns it as the function result
exit         
于 2018-01-25T15:11:33.110 回答