gcc 是不是会根据条件优化我的周期?

Posted

技术标签:

【中文标题】gcc 是不是会根据条件优化我的周期?【英文标题】:Does gcc optimize my cycle with condition?gcc 是否会根据条件优化我的周期? 【发布时间】:2011-04-02 11:38:33 【问题描述】:

我有以下循环:

//condition will be set here to true or false

for (int i = 0; i < LARGE_NUMBER; i++) 
    if (condition) 
        //do foo
     else 
        //do bar
    

假设:如果没有条件比有条件更快,则循环。 (这是真的?) 问题:如果 condition 已设置在 for 循环之外,并且循环本身不接触 condition,gcc 会排除我的 if 吗?

如果不是,我应该切换iffor,重复代码,违反DRY等

【问题讨论】:

如果'条件'没有改变,你为什么要把它放在'for'里面? @nacho4d:为了避免代码重复(for 标头,以及在if 之外但在for 内部的其他语句) 您是否将condition 声明为volatile @mvds: 条件不是易变的,否则编译器无法预测它的值。 我对答案非常满意,+1,谢谢大家。 【参考方案1】:

对于那些不想阅读冗长文章的人,这种优化称为(在 LLVM 中)Loop Unswitch。

为什么不问编译器?

void foo(char* c);

int main(int argc, char **argv) 
  bool const condition = argc % 2;

  for (int i = 0; i != argc; ++i) 
    if (condition) 
      foo(argv[1]);
     else 
      foo(argv[0]);
    
  
  return 0; 

转化为SSA形式(通过LLVM try out):

define i32 @main(i32 %argc, i8** nocapture %argv) 
entry:
  %0 = icmp eq i32 %argc, 0                       ; <i1> [#uses=1]
  br i1 %0, label %bb5, label %bb.nph

bb.nph:                                           ; preds = %entry
  %1 = and i32 %argc, 1                           ; <i32> [#uses=1]
  %toBool = icmp eq i32 %1, 0                     ; <i1> [#uses=1]
  %2 = getelementptr inbounds i8** %argv, i64 1   ; <i8**> [#uses=1]
  br i1 %toBool, label %bb3.us, label %bb3

bb3.us:                                           ; preds = %bb3.us, %bb.nph
  %i.07.us = phi i32 [ %4, %bb3.us ], [ 0, %bb.nph ] ; <i32> [#uses=1]
  %3 = load i8** %argv, align 8                   ; <i8*> [#uses=1]
  tail call void @_Z3fooPc(i8* %3)
  %4 = add nsw i32 %i.07.us, 1                    ; <i32> [#uses=2]
  %exitcond = icmp eq i32 %4, %argc               ; <i1> [#uses=1]
  br i1 %exitcond, label %bb5, label %bb3.us

bb3:                                              ; preds = %bb3, %bb.nph
  %i.07 = phi i32 [ %6, %bb3 ], [ 0, %bb.nph ]    ; <i32> [#uses=1]
  %5 = load i8** %2, align 8                      ; <i8*> [#uses=1]
  tail call void @_Z3fooPc(i8* %5)
  %6 = add nsw i32 %i.07, 1                       ; <i32> [#uses=2]
  %exitcond8 = icmp eq i32 %6, %argc              ; <i1> [#uses=1]
  br i1 %exitcond8, label %bb5, label %bb3

bb5:                                              ; preds = %bb3, %bb3.us, %entry
  ret i32 0

也许不太可读,所以让我指出这里的内容:

entry:检查argc是否等于0,如果是,则转到bb5(退出)否则转到bb.nph bb.nph:计算condition的值,如果为真,则转至bb3.us,否则转至bb3 bb3.usbb3:分别循环判断真假条件 bb5:退出

只要效果与您所要求的相似,编译器几乎可以将您的代码转换成它想要的样子。在这种情况下,它有效地将代码重写为:

int main(int argc, char**argv) 
  if (argc != 0)
  
    int i = 0;
    if (argc % 2) 
      do 
        foo(argv[1]);
        ++i;
       while (i != argc);
     else 
      do 
        foo(argv[0]);
        ++i;
       while (i != argc);
    
  
  return 0;

这是循环不变优化的一种形式,在这里结合第一次检查以避免在循环不会被执行时计算条件。

对于我们这些认为第一个解决方案更清晰的人来说,我们很高兴让编译器为我们进行细节优化!

【讨论】:

快速说明 事实上,如果编译器进入循环展开(以避免检查它是否必须在每次迭代中保持循环),它可能会变得更加丑陋。如果您想查看手动展开,请查看 Duff 的设备,您可能会同意编译器执行此操作会更好。【参考方案2】:

如果可以证明condition 在迭代期间不会发生变化,那么任何体面的优化编译器都会这样做。

此外,即使编译器实际上并没有这样做,您最好支持您的决定,即使用来自分析的硬数据将代码重写为人类可读性较差的形式。 不要过早优化。 给代码的读者一个“嗯?”是不值得的。为了缩短几毫秒的时间(“读者”肯定包括未来的你自己)。

【讨论】:

【参考方案3】:

我不主张在这里采取任何行动,通过通常的“过早优化”论点。保持代码清晰最重要,如果整个程序太慢,您可能需要在程序彻底调试后分析并找出实际的瓶颈(通常无法猜测)。

即使编译器没有为您优化这种特殊情况,您也可能想知道 CPU 会执行某种形式的 branch prediction,这将大大减少在条件可预测的情况下处理条件所需的时间。

事实上,大多数 CPU 处理指令都在 pipeline 中,并且在必须确定跳转地址时,条件变量可能是未知的。这会导致管道停顿,这是大多数现代处理器试图猜测(实际上是聪明地)程序将跳转到哪里的地方。如果条件变量确实是已知的(就像您的情况一样),那么猜测将是完美的。

所以我怀疑即使使用“愚蠢”的编译器,您实际上会看到这两个选项之间的差异,至少在现代机器上是这样。

【讨论】:

其实大部分编译器应该在这里写两个循环,根据条件的值,如果可以证明条件在循环执行过程中没有改变。它不会使您所说的无效。 完全同意马修。 +1 用于分支预测,在这种情况下确实使讨论变得毫无意义。

以上是关于gcc 是不是会根据条件优化我的周期?的主要内容,如果未能解决你的问题,请参考以下文章

如果我优化大小而不是速度,为什么GCC会生成15-20%的代码?

为啥在 GCC 5.1 中仍然启用 COW std::string 优化?

2812 恼人的青蛙(暴力搜索时根据问题优化判断条件以加快搜索速度)

根据恒定条件调用反应挂钩是不是安全?

GCC优化技巧,真的有用吗?

禁用 GCC 中的所有优化选项