为什么这个非常简单的 C# 方法会产生如此不合逻辑的 CIL 代码?
Why does this very simple C# method produce such illogical CIL code?
我最近一直在深入研究 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# 编译器不必担心优化?
您显示的输出是针对调试版本的。通过发布版本(或基本上打开优化),C# 编译器生成与您手动编写的相同的 IL。
我强烈怀疑这一切都是为了让调试器的工作更容易,基本上 - 让它更容易破解,并且在 return 编辑之前看到 return 值。
道德:当你想运行优化代码时,确保你没有要求编译器生成旨在调试的代码:)
我要写的并不是 .NET 特有的,而是一般性的,我不知道 .NET 在生成 CIL 时识别和使用的优化。语法树(以及语法分析器本身)识别具有以下词素的 return 语句:
returnStatement ::= RETURN expr ;
其中 returnStatement 和 expr 是非终结符而 RETURN 是终结符(return token)所以访问节点时对于常量 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
乔恩的回答当然是正确的;这个答案是跟进这个评论:
@EricLippert the local makes perfect sense, but is there any rationale for that br.s instruction, or is it just there out of convenience in the emitter code? I guess that if the compiler wanted to insert a breakpoint placeholder there, it could just emit a 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 中,代码生成简单 return 语句的模式是:
- 将您要 return 的值填入栈槽
- branch/leave到方法结束
- 在方法的最后,从槽中读取值并return
即未优化代码采用单点-return形式
在这种情况和原始发帖者显示的简单情况下,该模式都会导致生成 "branch to next" 情况。 "remove any branch to next" 优化器在生成未优化的代码时不会 运行,所以它仍然存在。
我最近一直在深入研究 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# 编译器不必担心优化?
您显示的输出是针对调试版本的。通过发布版本(或基本上打开优化),C# 编译器生成与您手动编写的相同的 IL。
我强烈怀疑这一切都是为了让调试器的工作更容易,基本上 - 让它更容易破解,并且在 return 编辑之前看到 return 值。
道德:当你想运行优化代码时,确保你没有要求编译器生成旨在调试的代码:)
我要写的并不是 .NET 特有的,而是一般性的,我不知道 .NET 在生成 CIL 时识别和使用的优化。语法树(以及语法分析器本身)识别具有以下词素的 return 语句:
returnStatement ::= RETURN expr ;
其中 returnStatement 和 expr 是非终结符而 RETURN 是终结符(return token)所以访问节点时对于常量 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
乔恩的回答当然是正确的;这个答案是跟进这个评论:
@EricLippert the local makes perfect sense, but is there any rationale for that br.s instruction, or is it just there out of convenience in the emitter code? I guess that if the compiler wanted to insert a breakpoint placeholder there, it could just emit a 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 中,代码生成简单 return 语句的模式是:
- 将您要 return 的值填入栈槽
- branch/leave到方法结束
- 在方法的最后,从槽中读取值并return
即未优化代码采用单点-return形式
在这种情况和原始发帖者显示的简单情况下,该模式都会导致生成 "branch to next" 情况。 "remove any branch to next" 优化器在生成未优化的代码时不会 运行,所以它仍然存在。