【问题标题】:MS C# compiler and non-optimized codeMS C# 编译器和非优化代码
【发布时间】:2010-09-07 13:37:38
【问题描述】:

注意:我注意到我发布的示例中有一些错误 - 编辑以修复它

如果您不启用优化,官方 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

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

【问题讨论】:

  • 我们需要等 Eric :)

标签: c# .net optimization compiler-construction cil


【解决方案1】:

我不完全理解问题的重点。听起来您在问“为什么当优化开关关闭时编译器会生成未优化的代码?”这有点回答自己。

但是,我会尝试一下。我认为这个问题实际上类似于“什么设计决策导致编译器发出声明、存储和加载本地#1,可以优化掉?”

答案是因为未优化的代码生成被设计为清晰、明确、易于调试,并鼓励抖动生成积极收集垃圾的代码。我们实现所有这些目标的方法之一是为堆栈上的大多数值生成 locals,甚至是 临时 值。让我们看一个更复杂的例子。假设你有:

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

现在抖动有一个很大的提示,上面写着“嘿,抖动,即使 Foo 不使用它,也要在本地保持一段时间

这里的一般规则是“使用未优化构建中的所有临时值制作局部变量”。这样你就可以了;为了评估“if”语句,我们需要评估一个条件并将其转换为布尔值。 (当然条件不必是 bool 类型;它可以是隐式可转换为 bool 的类型,或者是实现 operator true/operator false 对的类型。)未优化的代码生成器已被告知“积极地转换所有临时值进入当地人”,这就是你得到的。

我想在这种情况下,我们可以在“if”语句中的临时条件上抑制它,但这听起来像 为我做的工作对客户没有好处。因为只要您的手臂确实 有切实的客户利益,我就有一堆工作,我不会更改未优化的代码生成器,它会生成未优化的代码,就像它应该做的那样。

【讨论】:

    【解决方案2】:

    我没有真正看到问题,所有优化代码所做的只是优化单个引用的本地远离(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
    {
      ...
    }
    

    【讨论】:

    • 我注意到我发布的示例中有一些错误。对此感到抱歉..似乎不同之处如下。未优化:ceq、ldc.i4.0、ceq、stloc.1、ldloc.1、brtrue.s、do_not_do_something 已优化:bne.un.s do_not_do_something 这有点是有道理的。调试版本走了很长一段路,并反转从 ceq 返回的值,然后如果反转的值为真,则 branch 离开。优化后的版本直接调用br.un.s如果值为false就直接分支。
    【解决方案3】:

    具体的答案还需要等C#编译团队或者接近那个团队的人来详细解释这个案例。

    但是,这通常只是代码生成的产物,其中编写了通用例程来处理特定语句的许多不同情况,例如您的情况下的 if

    在某些情况下,这种概括会产生功能性但通常不是最佳的代码。这就是为什么存在优化通道以对生成的代码执行各种优化以删除冗余代码、循环展开、窥视孔优化、代码共享等的原因。

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

    【讨论】:

    • 好吧,我可以理解编译器在不优化时的辨别力要差得多,因此会生成次优代码。但我正在寻找的是它这样做的原因。 if 语句中的条件必须返回一个布尔值才能编译代码,那么为什么要与 0 比较呢?我真的想不出它不会是多余的。
    • @CPX,当然,由于我不熟悉C#编译器的code gen backend,所以这里只能提供场景。但是像生成代码以评估表达式并存储结果这样简单的事情,现在可以在 ifwhile 语句的代码生成中重用,尽管代码重用不太优化,实际上这可以帮助优化器,因为模式有更多的一致性,而不是由于复制/过去/演变造成的微小变化,因此编译器可以找到这些模式的优化。但是听到 MS 对此的看法会很有趣。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-03-11
    • 1970-01-01
    • 2011-04-13
    • 1970-01-01
    • 1970-01-01
    • 2012-06-08
    • 2016-02-14
    相关资源
    最近更新 更多