【问题标题】:Speed difference between If-Else and Ternary operator in C...?C中If-Else和三元运算符之间的速度差异......?
【发布时间】:2011-10-08 22:15:23
【问题描述】:

所以在同事的建议下,我刚刚测试了三元运算符和等效的 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)。

这是我想象出来的,还是有什么我没有考虑到的,或者这是依赖于机器的事情,还是什么?任何帮助表示赞赏。

【问题讨论】:

  • 关闭优化的基准毫无意义...
  • “顺便说一下,这一切都处于优化级别零 (-O0)。” 这意味着你已经告诉编译器不要优化(基本上),所以它是毫不奇怪,它将为更详细的代码生成更详细(因此更慢)的代码。如果你甚至向它扔一个-O1,我怀疑你根本不会注意到任何区别。在未优化代码中查看性能差异没有多大意义。
  • 注意:?是条件运算符,是三元运算符,不是三元运算符。
  • @Sani:它是 C++ 中唯一的三元运算符,因此是三元运算符。
  • @Sani:是的,美国未来可能会转向拥有多位现任总统的制度,但在那之前,奥巴马(或其他现任总统)是总统,而布什是总统。有趣的旧语言;-p

标签: c++ performance ternary-operator


【解决方案1】:

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

【讨论】:

  • IMO 这也应该是一个评论,考虑到前 2 个答案实际上是答案
  • 你是对的,但是......其中一个虽然认真回答了这个问题,但没有提到这只是-O0 的问题。编译器在优化方面做得很好,人们不应该做这样的微优化。关闭。
【解决方案2】:

三元运算符很有可能编译为cmov,而if/else 则编译为cmp+jmp。只需查看程序集(使用-S)即可确定。启用优化后,无论如何都不再重要,因为任何好的编译器都应该在两种情况下生成相同的代码。

【讨论】:

  • 在当今的 CPU 上,条件移动具有固定的延迟,而预测良好的条件分支基本上是免费的。因此,您可能不会看到优化编译器生成 CMOV 的频率与您想象的一样多。
【解决方案3】:

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

基本上,有“条件设置”处理器指令,这比在单独的指令中分支和设置要快。

【讨论】:

    【解决方案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

    结论是:没有区别

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

    【讨论】:

      【解决方案5】:

      您也可以完全无分支并测量是否有任何不同:

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

      在当今的架构中,这种编程风格已经有点过时了。

      【讨论】:

      • 当您在循环中有条件运行大量(例如数十亿)次时,它仍然很有用。
      【解决方案6】:

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

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

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

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

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

      【讨论】:

        猜你喜欢
        • 2013-11-09
        • 2013-06-24
        • 2022-12-06
        • 1970-01-01
        • 2010-12-12
        • 2021-01-03
        • 1970-01-01
        • 1970-01-01
        • 2018-02-22
        相关资源
        最近更新 更多