MS C#编译器和非优化代码

15

注意:我注意到我发布的示例中存在一些错误 - 现在已进行编辑以修复。

如果不开启优化,官方的C#编译器会做一些有趣的事情。

例如,一个简单的if语句:

int x;
// ... //
if (x == 10)
   // do something

如果进行了优化,它将变成以下内容:

ldloc.0
ldc.i4.s 10
ceq
bne.un.s do_not_do_something
// do something
do_not_do_something:

但是,如果我们禁用优化,它会变成这样:

ldloc.0
ldc.i4.s 10
ceq
ldc.i4.0
ceq
stloc.1
ldloc.1
brtrue.s do_not_do_something
// do something
do_not_do_something:

我不太理解这个。为什么会有那些额外的代码,它们似乎在源代码中并不存在?在C#中,这将是以下代码的等效形式:

int x, y;
// ... //
y = x == 10;
if (y != 0)
   // do something

有人知道为什么会这样吗?


8
我们需要等待Eric的到来 :) - Andrey
3个回答

21

我并不完全理解这个问题的意义。似乎你在问“为什么当优化开关关闭时,编译器会产生未经优化的代码?” 这有点回答了自己的问题。

然而,我仍将试着回答一下。我认为这个问题实际上是像“是什么设计决策导致编译器发出本地变量#1的声明、存储和加载,并可被优化掉?”这样的问题。

答案是因为未经优化的代码生成旨在清晰、明确、易于调试,并鼓励JIT生成不过度收集垃圾的代码。我们为实现所有这些目标之一,对大多数进入堆栈的值,甚至是临时值,都生成本地变量。 让我们看一个更复杂的例子。假设你有:

Foo(Bar(123), 456)
我们可以生成这样的内容:
push 123
call Bar - this pops the 123 and pushes the result of Bar
push 456
call Foo

这很好,高效而且小巧,但它不符合我们的目标。它清晰明了,但由于垃圾回收器可能会变得过于积极,所以调试起来并不容易。如果Foo因某些原因实际上并没有使用它的第一个参数,那么在Foo运行之前,GC可以允许回收Bar的返回值。

在未经优化的构建中,我们将生成更像这样的内容

push 123
call Bar - this pops the 123 and pushes the result of Bar
store the top of the stack in a temporary location - this pops the stack, and we need it back, so
push the value in the temporary location back onto the stack
push 456
call Foo

现在Jitter有一个大提示,它说“嘿Jitter,即使Foo不使用它,请在本地保持它的活动状态一段时间”。

这里的一般规则是“在未经优化的构建中将所有临时值转换为本地变量”。因此,在评估“if”语句之前,我们需要评估条件并将其转换为bool类型。(当然,条件不必是bool类型;它可以是隐式可转换为bool类型的类型,或者实现了operator true/operator false对的类型。)未经过优化的代码生成器被告知“积极地将所有临时值转换为本地变量”,所以你得到的就是这样的结果。

我想在这种情况下,我们可以抑制作为“if”语句中条件的临时值,但那听起来像是“让我多做一些没有客户利益的工作”。由于我有一堆需要带来实际客户利益的工作要做,我不会改变未经优化的代码生成器,它会生成未经优化的代码,正如它应该做的那样。


2
我并不认为有什么问题,所有优化的代码只是将一个本地引用进行了优化(即stloc ldloc组合)。
出现在调试版本中的原因是您可以在使用它之前查看分配给该本地变量的值。
编辑:我现在看到了另一个额外的ceq。
更新2:
我知道发生了什么。由于布尔值被表示为0和!0,调试版本执行第二个比较。另一方面,优化器可能可以证明代码的安全性。
未经优化的代码实际上会像这样:
int x, _local; // _local is really bool

_local = (x == 10) == 0;  // ceq is ==, not <, not sure why you see that
if (_local)  // as in C, iow _local != 0 implied
{
  ...
}

我注意到我发布的示例中有一些错误。对此感到抱歉。看起来区别在于以下内容。未经优化:ceq、ldc.i4.0、ceq、stloc.1、ldloc.1、brtrue.s、do_not_do_something。经过优化:bne.un.s do_not_do_something。这 有点 有道理。调试版本走了一个冗长的路线,并反转了从 ceq 返回的值,然后如果反转的值为 true,则 分支 走开。优化版本只需调用 br.un.s 直接分支,如果该值为 false。 - Christian Palmstierna

1

要得到具体的答案,您需要等待C#编译器团队中的某个人或接近该团队的人详细说明此情况。

然而,这通常只是代码生成的一种产物,在其中编写了常见例程以处理特定语句(例如您的情况中的if)的许多不同情况。

这种概括导致在某些情况下功能良好但常常不够优化的代码。这就是为什么存在优化过程来对生成的代码执行各种优化以消除冗余代码、循环展开、窥孔优化、代码共享等的原因。

在调试模式下编译时看到不太优化的代码的其他原因是为了支持调试器,例如可能会将NOP指令插入代码中以便在调试器中运行时设置断点,但在发布版本中将其删除。


我可以理解编译器在不进行优化时会变得不那么歧视,从而生成次优代码。但是我想知道的是它为什么会这样做。if语句中的条件必须返回布尔值才能编译,那么为什么要将其与0进行比较呢?我真的想不出有哪种情况下它不会是多余的。 - Christian Palmstierna
@CPX 当然了,我只能提供场景,因为我不熟悉C#编译器的代码生成后端。但是像生成代码来评估表达式并存储结果这样的简单操作,现在可以在ifwhile语句的代码生成中重复使用,虽然代码重用不太优化,但实际上这可以帮助优化器,因为模式更一致,不会因为复制/粘贴/演变而产生微小变化,从而使编译器可以找到并优化这些模式。但如果微软有自己的看法,那肯定很有趣。 - Chris Taylor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接