注意:我注意到我发布的示例中存在一些错误 - 进行编辑以修复它
如果不启用优化,官方 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
有谁知道为什么会这样?
我不完全理解问题的要点。听起来您好像在问“为什么当优化开关关闭时编译器会生成未优化的代码?”这有点回答自己。
不过,我会尝试一下。 我认为问题实际上类似于“什么设计决策导致编译器发出声明、存储和加载本地 #1,可以对其进行优化?”
答案是因为未优化的代码生成器被设计为清晰、明确、易于调试,并鼓励抖动生成“不”积极收集垃圾的代码。我们实现所有这些目标的方法之一是为堆栈上的大多数值(甚至是临时值)生成“局部变量”。让我们看一个更复杂的例子。假设你有:
Foo(Bar(123), 456)
我们可以将其生成为:
push 123
call Bar - this pops the 123 and pushes the result of Bar
push 456
call Foo
这很好、高效、小,但它没有达到我们的目标。它清晰明确,但不易调试,因为垃圾收集器可能会变得激进。如果 Foo 由于某种原因实际上没有对其第一个参数执行任何操作,则允许 GC 在 Foo 运行之前回收 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
现在抖动有一个很大的提示,说“嘿抖动,
”
这里的一般规则是“从未优化构建中的所有临时值中创建局部变量”。就这样吧;为了评估“if”语句,我们需要评估条件并将其转换为布尔值。 (当然,条件不必是 bool 类型;它可以是可隐式转换为 bool 的类型,或者是实现运算符 true/operator false 对的类型。)未优化的代码生成器被告知“积极地转换所有临时值”融入当地人”,这就是你得到的。
我想在这种情况下,我们可以在“if”语句中的临时条件上抑制这一点,但这听起来像是为我做没有客户利益的工作。 由于只要您的手臂
确实具有切实的客户利益,我就有一堆工作要做,所以我不会更改未优化的代码生成器,它会生成未优化的代码,正如它应该的那样。
我真的没有看到这个问题,所有优化的代码所做的都是优化单个引用的本地(stloc ldloc 组合)。 它出现在调试版本中的原因是您可以在使用它之前看到分配给本地的值。
ceq
。
更新2:
我明白发生了什么。由于布尔值表示为 0 和 !0,调试版本会进行第二次比较。 OTOH,优化器可能可以证明代码的安全性。
未优化的代码实际上是这样的:
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
{
...
}
具体答案你需要等待 C# 编译器团队中的某个人或与该团队关系密切的人对此案例进行详细解释。
但是,这通常只是代码生成的产物,其中编写通用例程来处理特定语句的许多不同情况,例如您的情况中的
if
在某些情况下,这种泛化会产生功能性但通常不是最佳的代码。这就是为什么存在优化遍来对生成的代码执行各种优化,以删除冗余代码、循环展开、窥视孔优化、代码共享等。
在调试模式下编译时看到不太理想的代码的其他原因是为了支持调试器,例如,在调试器中运行时可能会在代码中插入 NOP 指令以方便设置断点,但在发布版本中被删除。