为啥这个非常简单的 C# 方法会产生如此不合逻辑的 CIL 代码?
Posted
技术标签:
【中文标题】为啥这个非常简单的 C# 方法会产生如此不合逻辑的 CIL 代码?【英文标题】:Why does this very simple C# method produce such illogical CIL code?为什么这个非常简单的 C# 方法会产生如此不合逻辑的 CIL 代码? 【发布时间】:2018-07-04 21:05:14 【问题描述】:我最近一直在研究 IL,我注意到 C# 编译器的一些奇怪行为。以下方法是一个非常简单且可验证的应用程序,它会立即以退出代码 1 退出:
static int Main(string[] args)
return 1;
当我使用 Visual Studio Community 2015 编译它时,会生成以下 IL 代码(添加了 cmets):
.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#编译器不用担心优化?
【问题讨论】:
你是在调试还是发布模式下编译? 【参考方案1】:您显示的输出用于调试构建。使用发布版本(或基本上打开优化),C# 编译器会生成与您手动编写的相同的 IL。
我强烈怀疑这一切都是为了让调试器的工作更容易,基本上 - 让它更容易破解,并在返回之前查看返回值。
道德:当您想运行优化代码时,请确保您没有要求编译器生成旨在调试的代码 :)
【讨论】:
这很有意义,谢谢!正如所料,在发布模式下编译时,IL 完全符合预期 Jon 的怀疑当然是正确的。 @lpmitchell:你会在未优化的代码中看到很多可能是“短暂”的值——也就是说,只是推到评估堆栈上,然后在我们完成它们时弹出——而是存储和读取从特定的局部变量堆栈槽。这对数字 1 没有任何作用,但想象一下是否有一个对象引用;存储和检索它可以大大降低 GC 比您预期更早收集对象的可能性,这有助于调试。 @EricLippert 本地非常有意义,但是该br.s
指令是否有任何理由,或者它只是为了方便发射器代码?我猜如果编译器想在那里插入一个断点占位符,它可以发出一个nop
...
@LucasTrzesniewski:我已经发布了一个回答你的问题。【参考方案2】:
乔恩的回答当然是正确的;这个答案是为了跟进这个评论:
@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 中,代码生成简单返回语句的模式是:
即未优化的代码使用单点返回形式。
在这种情况下和原始海报所示的简单情况下,该模式都会导致生成“分支到下一个”情况。 “删除任何分支到下一个”优化器在生成未优化的代码时不会运行,所以它仍然存在。
【讨论】:
【参考方案3】:我要写的并不是 .NET 特定的,而是通用的,我不知道 .NET 在生成 CIL 时识别和使用的优化。语法树(以及语法解析器本身)识别具有以下词位的 return 语句:
returnStatement ::= RETURN expr ;
returnStatement 和 expr 是非终结符,RETURN 是终结符(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
【讨论】:
您忘记了一个非常常见的优化,称为constant folding。当编译器看到 1+1 时,它知道无论如何都会是 2。因此,不是让程序在运行时添加 1 和 1,而是在编译期间添加一次。所以在你的伪代码中,两条push const_1
行将被一个push const_2
行替换。以上是关于为啥这个非常简单的 C# 方法会产生如此不合逻辑的 CIL 代码?的主要内容,如果未能解决你的问题,请参考以下文章