5

有人可以解释这几行 MSIL 吗?为什么它将评估堆栈中的值移到局部变量中,只是立即将其移回并返回?

以下 MSIL 代码加载单个参数(字符串),调用返回 bool 的方法,然后返回该 bool 值。我不明白为什么它调用 stloc.0 将方法的返回值存储在局部变量中,然后执行显式无条件控制转移到下一个标记行(似乎没有必要),只是将值移回返回之前的评估堆栈。

.maxstack 1
.locals init ([0] bool CS$1$0000)
L_0000: nop
L_0001: ldarg.0
L_0002: call bool FuncNameNotImporant::MethodNameNotImporant(string)
L_0007: stloc.0 
L_0008: br.s L_000a
L_000a: ldloc.0 
L_000b: ret 

我对它为什么这样做的最佳猜测是执行某种类型检查以确保评估堆栈上的值在返回之前实际上是一个布尔值。但是我对明确跳转到下一行一无所知;我的意思是,它不会去那里吗?该方法的 C# 源代码只有一行,它返回该方法的结果。

4

2 回答 2

7

如果您在调试器中打开此函数,并在调试模式下编译代码:

bool foo(string arg)
{
    return bar(arg);
}

您可以设置 3 个断点:

  1. 在函数的左大括号处。
  2. 在“返回”线上。
  3. 在函数的右大括号处。

在左大括号上设置断点意味着“在调用此函数时中断”。这就是为什么在方法的开头有一个无操作指令的原因。当断点设置在左大括号上时,调试器实际上将它设置为空操作。

在右大括号上设置断点意味着“此函数退出时中断”。为了实现这一点,函数需要在它的 IL 中有一个返回指令,可以在其中设置断点。编译器通过使用临时变量来存储返回值并转换

return retVal;

进入

$retTmp = retVal;
goto exit;

然后在方法的底部注入以下代码:

exit:
return $ret;

此外,在调试模式下,编译器对他们生成的代码很愚蠢。他们基本上会做类似的事情:

GenerateProlog();
foreach (var statement in statements)
{
    Generate(statement);
}
GenerateEpilog();

在您的情况下,您会看到:

return foo(arg);

被翻译成:

; //this is a no-op
bool retTemp = false;
retTemp = foo(arg);
goto exit;
exit:
return retTemp;

如果编译器正在执行“滑动窗口优化”,它可能会查看该代码并意识到存在一些冗余。但是,编译器通常不会在调试模式下执行此操作。编译器优化可以做诸如消除变量和重新排序指令之类的事情,这使得调试变得困难。由于调试构建的目的是启用调试,因此打开优化并不好。

在发布版本中,代码不会像那样。那是因为编译器没有引入特殊代码来启用左大括号和右大括号上的断点,只留下以下代码进行编译:

return bar(arg);

最终看起来很简单。

然而,需要注意的一件事是,我认为 C# 编译器不会对滑动窗口进行太多优化,即使在零售版本中也是如此。那是因为大多数优化依赖于底层处理器架构,因此由 JIT 编译器完成。在 C# 编译器中进行优化,即使是与处理器无关的优化,也会阻碍 JIT 优化代码的能力(它正在寻找由非优化代码生成生成的模式,如果它看到高度优化的 IL,它可以获得使困惑)。所以通常托管代码编译器不会这样做。它做了一些“昂贵的事情”(JIT 不想在运行时做),比如死代码检测和实时变量分析,但它们并没有解决滑动窗口优化解决的问题。

于 2009-05-19T23:16:32.827 回答
4

你是在调试还是发布模式下编译?在发布模式下,我得到:

.method private hidebysig static bool Test1(string arg) cil managed
{
    .maxstack 8
    L_0000: ldarg.0 
    L_0001: call bool FuncNameNotImportant::MethodNameNotImportant(string)
    L_0006: ret 
}

您看到的分支可能是用于调试器支持。

于 2009-05-18T22:35:18.420 回答