【问题标题】:Will the compiler optimize escaping an inner loop?编译器会优化转义内部循环吗?
【发布时间】:2011-03-04 06:27:46
【问题描述】:

我的代码看起来像这样(显示了 done 的所有用法):

bool done = false;
for(int i = 0; i < big; i++)
{
  ...
  for(int j = 0; j < wow; j++)
  {
    ...
    if(foo(i,j))
    {
       done = true;
       break;
    }
    ...
  }
  if(done) break;
  ...
}

任何编译器都会把它转换成这个:

for(int i = 0; i < big; i++)
{
  ...
  for(int j = 0; j < wow; j++)
  {
    ...
    if(foo(i,j))
      goto __done; // same as a labeled break if we had it
    ...
  }
  ...
}
__done:;

注意:虽然我最感兴趣的是 if(done)break; 是否被绕过并作为死代码删除,但我也对它和 done 是否被完全删除感兴趣。 p>

【问题讨论】:

  • 顺便说一句,你不应该定义任何以两个下划线开头的符号;此类符号是保留的。
  • 该符号将是优化过程的结果,例如由编译器生成。我使用该名称因为它表示保留/内部名称。
  • 还有一个问题:为什么不把它隐藏在一个函数中?然后你可以使用return ;)
  • 带标签的 break 在 C/C++ 中称为 GOTO 语句,适合您的情况,除非您想将代码分解为单独的函数。
  • @fabspro:为了这个问题,假设禁止使用goto(例如某些PHB)。将循环放在另一个函数中也是如此。

标签: c++ optimization compiler-construction


【解决方案1】:

我并不想刻薄,但是……这有关系吗?一般来说,我认为最好让编译器完成他们的工作,而这项工作是根据您的源代码生成“最佳”(请注意,“最佳”可能会因您的需要而异)编译代码。代码中的任何性能考虑都应该通过分析器和算法复杂性的良好工作知识来识别。

如果您只是好奇,请忽略此评论。但是,如果您的意图是以某种方式优化您的代码,那么我认为还有更好的途径。

【讨论】:

  • 许多编译器并没有很好地完成这项工作——至少没有我们想象的那么好。
  • @Crashworks,对特定编译器进行微优化的问题在于,它是特定于编译器的。如果您主动阻止它进行一些新的优化,您的代码可能会在所述编译器的后续版本中显着退化。最好在您知道编译器由于语言限制(过多的对象复制、别名等)而无法优化的地方进行优化。在这一点上,很难知道哪些优化没有完成是因为缺少编译器,哪些是因为语言规范不允许这样做。
  • 我有点同意,Crash,但我仍然认为从根本上重要的是理解算法的复杂性。世界上最好的编译器无法修复一些劣质的线性搜索或糟糕的字符串连接代码。我发现将编译器视为一个黑匣子会更有成效,因为它迫使我们对我们最能控制的东西负责:我们自己的代码。
  • 算法和内存层次绝对是最重要的!值得记住的是,编译器并不完美,有时分析器会发现你真正可以改进的东西。
【解决方案2】:

我已尝试使用 GCC 4.2.1 进行以下操作:

// Prevent optimizing out calls to foo and loop unrolling:
extern int big, wow;
bool foo(int,int);

void
bar()
{
    int done = false;
    for(int i = 0; i < big; i++)
    {
        for(int j = 0; j < wow; j++)
        {
            if(foo(i,j))
            {
                done = true;
                break;
            }
        }
        if(done)
            break;
    }
}

...它直接通过-O3 发布:

  33:   e8 fc ff ff ff          call   34 <bar()+0x34> ; call to foo*
  38:   84 c0                   test   %al,%al
  3a:   74 e5                   je     21 <bar()+0x21> ; next loop iteration
  3c:   83 c4 10                add    $0x10,%esp
  3f:   5b                      pop    %ebx
  40:   5e                      pop    %esi
  41:   5d                      pop    %ebp
  42:   c3                      ret

*** 这是来自一个未链接的目标文件,call 34 实际上是对foo 的调用。

【讨论】:

  • 所以3a 直接分支到42 的终极终点?
  • @BCS, 如果 AL 为零(foo 返回 false),则 0x3a 跳转到循环的开头,否则它只是继续后同步(弹出保存的寄存器,恢复先前的堆栈帧) 3c, 3f, . . 直到退役
【解决方案3】:

GNU 编译器就是这样做的,从优化级别 -O1 开始(我在 x86_64 上使用 gcc 4.5.1)

call    _Z3fooii  // al = foo(i,j)
testb   %al, %al
jne .L14
...

其中 .L14 是准确放置在您放置 __done 的位置的标签:

一个更好的问题可能是:哪个现代编译器执行这种优化?

【讨论】:

  • SNC -- 至少不是我们拥有的版本。
【解决方案4】:

显然这取决于编译器。当您不确定时,最好的办法是查看编译器的汇编输出(所有流行的编译器都有一个开关)。即使你不熟悉汇编,你至少可以将调试版本与优化版本进行比较。

话虽如此,这是goto 不是一个坏主意的少数情况之一。随意使用它来打破内部循环。

编辑

刚刚在VS2010中尝试了以下,它确实优化了外部条件:

bool done = false;
for(int i = 0; i < 10; i++)
{
    for(int j = 0; j < 10; j++)
    {
        if(i == 7 && j == 3)
        {
            done = true;
            break;
        }
    }
    if(done) break;
}
return 0;

【讨论】:

  • 实用主义+1。太多的人忽视了这样一个事实,即 goto、break、函数的多个返回点以及其他类似的东西,只有当它们使代码难以理解时才是不好的。明智地使用这些东西很好。
  • 大胆确保没有人错过 :)
  • 是的,goto 在那里是合法的。 OTOH 标记的 break 甚至更好,如果它避免我在代码审查中为它辩护,我将与编译器一起为我做一些魔法。
  • 我认为大多数编译器都可以很容易地优化它,因为代码结尾类似于:set var, true; goto innerloopend (*); ... innerloopend: cmp var, true; beq outerloopend 和 "tracking" var 很明显,在 "goto innerloopend (*) 中将始终导致 goto outerloopend”,因为从 (*) 到达时,cmp var, true 将始终为真。大多数编译器的轻松优化
  • 好吧,我认为编译器找到它并不是那么“容易”(你能告诉我如何)但是即使是 msvc++ 也能找到它(即使它不能优化那么多)一般)。
猜你喜欢
  • 1970-01-01
  • 2017-01-27
  • 1970-01-01
  • 2012-12-11
  • 1970-01-01
  • 2017-11-05
  • 1970-01-01
  • 1970-01-01
  • 2011-10-30
相关资源
最近更新 更多