有人可以解释这几行MSIL吗?

时间:2022-08-10 16:49:00

Can someone explain these few lines of MSIL? Why does it move a value off the evaluation stack to a local variable, only to move it back immediately and return it?

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

The following MSIL code loads a single argument (a string), calls a method, which returns bool, and then returns that bool value. What I don't understand is why it calls stloc.0 to store the method's return value in a local variable, then performs an explicit unconditional control transfer to the very next labeled line (seems unnecessary), only to move the value right back onto the evaluation stack before returning it.

以下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 

My best guess at why it does this is to perform some kind of type check to ensure the value on the evaluation stack is actually a boolean value before returning it. But I am clueless about the explicit jump to the very next line; I mean, wouldn't it go there anyway? The C# source code for the method is just one line, which returns the result of the method.

我最好猜测它为什么这样做是为了确保评估堆栈上的值在返回之前实际上是一个布尔值。但我对明确跳到下一行是毫无头绪的;我的意思是,它不会去那里吗?该方法的C#源代码只有一行,它返回方法的结果。

2 个解决方案

#1


If you open this function in the debugger, with code compiled in debug mode:

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

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

There are 3 break points you can set:

您可以设置3个断点:

  1. At the opening brace of the function.
  2. 在功能的开头大括号。

  3. On the "return" line.
  4. 在“回归”线上。

  5. At the closing brace of the function.
  6. 在功能的右大括号。

Setting a break point on the opening brace means "break when this function get's called". That's why there is a no-op instruction at the beginning of the method. When the breakpoint is set on the opening brace the debugger actually sets it on the no-op.

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

Setting a break point on the closing brace means "break when this function exits". In order for this to happen the function needs to have a single return instruction in it's IL, where the break point can be set. The compiler enables that by using a temporary variable to store the return value, and converting

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

return retVal;

into

$retTmp = retVal;
goto exit;

and then injecting the following code at the bottom of the method:

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

exit:
return $ret;

Also, when in debug mode, compilers are stupid about the code they generation.They basically do something like:

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

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

In your case, you are seeing:

在您的情况下,您看到:

return foo(arg);

being translated into:

被翻译成:

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

If the compiler was doing a "sliding window optimization" it might be able to look at that code and realize there was some redundency.However, compilers generally don't do that in debug mode. Compiler optimizations can do things like eliminate variables, and reorder instructions, which makes debugging difficult. Since the purpose of a debug build is to enable debugging, it would not be good to turn on optimizations.

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

In a release build, the code will not look like that. That's because the compiler does not introduce the special code to enable breakpoints on the opening and closing braces, which just leaves the following to be compiled:

在发布版本中,代码看起来不像那样。那是因为编译器没有引入特殊代码来在开始和结束括号上启用断点,这只留下以下内容进行编译:

return bar(arg);

That ends up looking pretty simple.

最终看起来非常简单。

One thing to note, however, is that I don't think the C# compiler does much sliding window optimizations, even in retail builds. Thats because most of those optimizations depend on the underlying processor architecture and so are done by the JIT compiler. Doing the optimizations, even the ones that are processor agnostic, in the C# compiler can impede the JIT's ability to optimize the code (it's looking for patterns that are generated by non-optimized code generation, and if it sees heavily optimized IL it can get confused). So usually manged code compilers don't do them. It does some "expensive things" (that the JIT doesn't want to do at runtime), like dead code detection, and live variable analysis, but they don't address the problems solved by sliding window optimization.

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

#2


Are you compiling in debug or release mode? In release mode I get:

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

.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 
}

The branching you're seeing is probably for debugger support.

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

#1


If you open this function in the debugger, with code compiled in debug mode:

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

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

There are 3 break points you can set:

您可以设置3个断点:

  1. At the opening brace of the function.
  2. 在功能的开头大括号。

  3. On the "return" line.
  4. 在“回归”线上。

  5. At the closing brace of the function.
  6. 在功能的右大括号。

Setting a break point on the opening brace means "break when this function get's called". That's why there is a no-op instruction at the beginning of the method. When the breakpoint is set on the opening brace the debugger actually sets it on the no-op.

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

Setting a break point on the closing brace means "break when this function exits". In order for this to happen the function needs to have a single return instruction in it's IL, where the break point can be set. The compiler enables that by using a temporary variable to store the return value, and converting

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

return retVal;

into

$retTmp = retVal;
goto exit;

and then injecting the following code at the bottom of the method:

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

exit:
return $ret;

Also, when in debug mode, compilers are stupid about the code they generation.They basically do something like:

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

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

In your case, you are seeing:

在您的情况下,您看到:

return foo(arg);

being translated into:

被翻译成:

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

If the compiler was doing a "sliding window optimization" it might be able to look at that code and realize there was some redundency.However, compilers generally don't do that in debug mode. Compiler optimizations can do things like eliminate variables, and reorder instructions, which makes debugging difficult. Since the purpose of a debug build is to enable debugging, it would not be good to turn on optimizations.

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

In a release build, the code will not look like that. That's because the compiler does not introduce the special code to enable breakpoints on the opening and closing braces, which just leaves the following to be compiled:

在发布版本中,代码看起来不像那样。那是因为编译器没有引入特殊代码来在开始和结束括号上启用断点,这只留下以下内容进行编译:

return bar(arg);

That ends up looking pretty simple.

最终看起来非常简单。

One thing to note, however, is that I don't think the C# compiler does much sliding window optimizations, even in retail builds. Thats because most of those optimizations depend on the underlying processor architecture and so are done by the JIT compiler. Doing the optimizations, even the ones that are processor agnostic, in the C# compiler can impede the JIT's ability to optimize the code (it's looking for patterns that are generated by non-optimized code generation, and if it sees heavily optimized IL it can get confused). So usually manged code compilers don't do them. It does some "expensive things" (that the JIT doesn't want to do at runtime), like dead code detection, and live variable analysis, but they don't address the problems solved by sliding window optimization.

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

#2


Are you compiling in debug or release mode? In release mode I get:

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

.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 
}

The branching you're seeing is probably for debugger support.

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