C语言中If-Else和Ternary运算符之间的速度差异?

时间:2011-07-19 21:33:55

标签: c++ performance ternary-operator

所以在一位同事的建议下,我刚刚测试了三元运算符和等效的If-Else块之间的速度差异......似乎三元运算符产生的代码比If-快1倍到2倍之间其他。我的代码是:

  gettimeofday(&tv3, 0);
  for(i = 0; i < N; i++)
  {
     a = i & 1;
     if(a) a = b; else a = c;
  }
  gettimeofday(&tv4, 0);


  gettimeofday(&tv1, 0);
  for(i = 0; i < N; i++)
  {
     a = i & 1;
     a = a ? b : c;
  }
  gettimeofday(&tv2, 0);

(抱歉使用gettimeofday而不是clock_gettime ......我会努力改善自己。)

我尝试更改计时块的顺序,但结果似乎仍然存在。是什么赋予了?此外,If-Else在执行速度方面表现出更多的可变性。我应该检查gcc生成的程序集吗?

顺便说一下,这一切都处于优化级别零(-O0)。

我想象这个,或者有什么我没有考虑到的,或者这是机器相关的东西,还是什么?任何帮助表示赞赏。

6 个答案:

答案 0 :(得分:24)

很有可能将三元运算符编译为cmov,而if / else结果为cmp + jmp。只需看一下装配(使用-S)就可以了。启用优化后,无论如何都无关紧要,因为任何好的编译器都应该在两种情况下生成相同的代码。

答案 1 :(得分:9)

这是一个很好的解释:http://www.nynaeve.net/?p=178

基本上,有“条件设置”处理器指令,它比分支指令和分支指令更快。

答案 2 :(得分:9)

你也可以完全无分支,并衡量它是否有任何区别:

int m = -(i & 1);
a = (b & m) | (c & ~m);

在今天的架构上,这种编程风格已经过时了。

答案 3 :(得分:4)

如果有,请更改编译器!

对于这类问题,我使用Try Out LLVM页面。这是LLVM的旧版本(仍然使用gcc前端),但这些都是旧技巧。

这是我的小样本程序(你的简化版):

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

int main (int argc, char* argv[]) {
  int N = atoi(argv[0]);

  int a = 0, d = 0, b = atoi(argv[1]), c = atoi(argv[2]);

  int i;
  for(i = 0; i < N; i++)
  {
     a = i & 1;
     if(a) a = b+i; else a = c+i;
  }

  for(i = 0; i < N; i++)
  {
     d = i & 1;
     d = d ? b+i : c+i;
  }

  printf("%d %d", a, d);

  return 0;
}

并且生成了相应的LLVM IR:

define i32 @main(i32 %argc, i8** nocapture %argv) nounwind {
entry:
  %0 = load i8** %argv, align 8                   ; <i8*> [#uses=1]
  %N = tail call i32 @atoi(i8* %0) nounwind readonly ; <i32> [#uses=5]

  %2 = getelementptr inbounds i8** %argv, i64 1   ; <i8**> [#uses=1]
  %3 = load i8** %2, align 8                      ; <i8*> [#uses=1]
  %b = tail call i32 @atoi(i8* %3) nounwind readonly ; <i32> [#uses=2]

  %5 = getelementptr inbounds i8** %argv, i64 2   ; <i8**> [#uses=1]
  %6 = load i8** %5, align 8                      ; <i8*> [#uses=1]
  %c = tail call i32 @atoi(i8* %6) nounwind readonly ; <i32> [#uses=2]

  %8 = icmp sgt i32 %N, 0                         ; <i1> [#uses=2]
  br i1 %8, label %bb, label %bb11

bb:                                               ; preds = %bb, %entry
  %9 = phi i32 [ %10, %bb ], [ 0, %entry ]        ; <i32> [#uses=2]
  %10 = add nsw i32 %9, 1                         ; <i32> [#uses=2]
  %exitcond22 = icmp eq i32 %10, %N               ; <i1> [#uses=1]
  br i1 %exitcond22, label %bb10.preheader, label %bb

bb10.preheader:                                   ; preds = %bb
  %11 = and i32 %9, 1                             ; <i32> [#uses=1]
  %12 = icmp eq i32 %11, 0                        ; <i1> [#uses=1]
  %.pn13 = select i1 %12, i32 %c, i32 %b          ; <i32> [#uses=1]
  %tmp21 = add i32 %N, -1                         ; <i32> [#uses=1]
  %a.1 = add i32 %.pn13, %tmp21                   ; <i32> [#uses=2]
  br i1 %8, label %bb6, label %bb11

bb6:                                              ; preds = %bb6, %bb10.preheader
  %13 = phi i32 [ %14, %bb6 ], [ 0, %bb10.preheader ] ; <i32> [#uses=2]
  %14 = add nsw i32 %13, 1                        ; <i32> [#uses=2]
  %exitcond = icmp eq i32 %14, %N                 ; <i1> [#uses=1]
  br i1 %exitcond, label %bb10.bb11_crit_edge, label %bb6

bb10.bb11_crit_edge:                              ; preds = %bb6
  %15 = and i32 %13, 1                            ; <i32> [#uses=1]
  %16 = icmp eq i32 %15, 0                        ; <i1> [#uses=1]
  %.pn = select i1 %16, i32 %c, i32 %b            ; <i32> [#uses=1]
  %tmp = add i32 %N, -1                           ; <i32> [#uses=1]
  %d.1 = add i32 %.pn, %tmp                       ; <i32> [#uses=1]
  br label %bb11

bb11:                                             ; preds = %bb10.bb11_crit_edge, %bb10.preheader, %entry
  %a.0 = phi i32 [ %a.1, %bb10.bb11_crit_edge ], [ %a.1, %bb10.preheader ], [ 0, %entry ] ; <i32> [#uses=1]
  %d.0 = phi i32 [ %d.1, %bb10.bb11_crit_edge ], [ 0, %bb10.preheader ], [ 0, %entry ] ; <i32> [#uses=1]
  %17 = tail call i32 (i8*, ...)* @printf(i8* noalias getelementptr inbounds ([6 x i8]* @.str, i64 0, i64 0), i32 %a.0, i32 %d.0) nounwind ; <i32> [#uses=0]
  ret i32 0
}

好吧,所以它可能是中文,即使我继续并重命名了一些变量以使其更容易阅读。

重要的是这两个块:

  %.pn13 = select i1 %12, i32 %c, i32 %b          ; <i32> [#uses=1]
  %tmp21 = add i32 %N, -1                         ; <i32> [#uses=1]
  %a.1 = add i32 %.pn13, %tmp21                   ; <i32> [#uses=2]

  %.pn = select i1 %16, i32 %c, i32 %b            ; <i32> [#uses=1]
  %tmp = add i32 %N, -1                           ; <i32> [#uses=1]
  %d.1 = add i32 %.pn, %tmp                       ; <i32> [#uses=1]

分别设置ad

结论是:没有区别

注意:在一个更简单的例子中,两个变量实际上已合并,似乎优化器没有检测到相似性......

答案 4 :(得分:0)

如果打开优化,任何体面的编译器都应为这些代码生成相同的代码。

答案 5 :(得分:0)

理解它完全取决于编译器如何解释三元表达式(除非你实际强制它不使用(内联)asm)。它可以很容易地将三元表达理解为其内部表示语言中的“if..else”,并且根据目标后端,它可以选择生成条件移动指令(在x86上,CMOVcc就是这样。还应该有一个最小/最大,绝对等)。使用条件移动的主要动机是将分支错误预测的风险转移到存储器/寄存器移动操作。对该指令的警告是,几乎所有时间,将有条件地加载的操作数寄存器必须被评估为寄存器形式以利用cmov指令。

这意味着无条件评估过程现在必须是无条件的,这似乎会增加程序的无条件路径的长度。但要明白,分支错误预测通常被解决为“刷新”管道,这意味着将完成执行的指令被忽略(转向无操作指令)。这意味着由于停顿或NOP,执行的实际指令数量更高,并且效果随着处理器流水线的深度和误预测率而变化。

这给确定正确的启发式方法带来了一个有趣的困境。首先,我们确信如果管道太浅或分支预测完全能够从分支历史中学习模式,那么cmov就不值得去做。如果有条件参数的评估成本高于平均误预测的成本,那么这也是不值得做的。

这些可能是编译器难以利用cmov指令的核心原因,因为启发式确定在很大程度上取决于运行时分析信息。在JIT编译器上使用它更有意义,因为它可以提供运行时检测反馈并构建更强的启发式来使用它(“分支是否真的不可预测?”)。在没有训练数据或分析器的静态编译器端,最难以假设它何时有用。但是,如前所述,如果编译器知道数据集完全是随机的或强制cond,那么简单的否定启发式就是如此。无条件。评估成本很高(可能是由于不可减少的,昂贵的操作,如fp划分),如果不这样做,那将是很好的启发式。

任何有价值的编译器都会做到这一切。问题是,在所有可靠的启发式算法用完之后它会做什么......