我最近一直在挖掘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#编译器不必担心优化?
答案 0 :(得分:23)
您显示的输出是用于调试版本。使用发布版本(或基本上启用了优化),C#编译器会生成您手动编写的相同IL。
我强烈怀疑这一切都是为了让调试器的工作变得更容易,基本上 - 让它更容易中断,并在返回之前看到返回值。
道德:当你想运行优化代码时,请确保你没有要求编译器生成旨在调试的代码:)
答案 1 :(得分:11)
@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中,codegen的简单返回语句的模式是:
也就是说,未经优化的代码使用单点返回形式。
在这种情况下和原始海报所显示的简单情况下,该模式会导致下一个"分支到下一个"要生成的情况。 "删除任何分支到下一个"生成未经优化的代码时,优化器不会运行,因此它仍然存在。
答案 2 :(得分:-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