提问者:小点点

免费分支预测?


我刚刚偶然发现了这个东西,我真的很好奇现代CPU(当前的CPU,可能还有移动CPU(嵌入式))在下面的情况下是否实际上没有分支成本。

1.假设我们有这个:

x += a; // let's assume they are both declared earlier as simple ints  
if (flag)  
   do A  // let's assume A is not the same as B  
else  
   do B  // and of course B is different than A  

2.与此相比:

if (flag)  
{  
  x += a   
  do A  
}  
else  
{  
   x += a  
   do B  
}

假设AB在流水线指令(获取、解码、执行等)的温度上完全不同:

>

  • 第二种方法会更快吗?

    CPU是否足够聪明,可以判断出无论标志是什么,下一条指令都是相同的(因此他们不必因为分支未命中预测而放弃流水线阶段)?

    在第一种情况下,CPU没有选择,但是如果发生分支未命中预测,则放弃doA或doB的前几个管道阶段,因为它们是不同的。我认为第二个例子是某种延迟的分支,比如:“我要检查那个标志,即使我不知道这个标志,我也可以继续下一条指令,因为它是一样的,不管标志是什么,我已经有了下一条指令,我可以使用它。”

    编辑:
    我做了一些研究,我有一些不错的结果。你会如何解释这种行为?抱歉,我最近的编辑,但据我所知,我有一些缓存问题,我希望这些是更准确的结果和代码示例。

    这是使用-O3使用gcc版本4.8.2(Ubuntu 4.8.2-19ubuntu1)编译的代码。

    案例1。

    #include <stdio.h>
    
    extern int * cache;
    extern bool * b;
    extern int * x;
    extern int * a;
    extern unsigned long * loop;
    
    extern void A();
    extern void B();
    
    int main()
    {
        for (unsigned long i = 0; i < *loop; ++i)
        {
            ++*cache;
    
            *x += *a;
    
            if (*b)
            {
                A();
            }
            else
            {
                B();
            }
        }
    
        delete b;
        delete x;
        delete a;
        delete loop;
        delete cache;
    
        return 0;
    }
    
    int * cache = new int(0);
    bool * b = new bool(true);
    int * x = new int(0);
    int * a = new int(0);
    unsigned long * loop = new unsigned long(0x0ffffffe);
    
    void A() { --*x; *b = false; }
    void B() { ++*x; *b = true; }
    

    案例2

    #include <stdio.h>
    
    extern int * cache;
    extern bool * b;
    extern int * x;
    extern int * a;
    extern unsigned long * loop;
    
    extern void A();
    extern void B();
    
    int main()
    {
        for (unsigned long i = 0; i < *loop; ++i)
        {
            ++*cache;
    
            if (*b)
            {
                *x += *a;
                A();
            }
            else
            {
                *x += *a;
                B();
            }
        }
    
        delete b;
        delete x;
        delete a;
        delete loop;
        delete cache;
    
        return 0;
    }
    
    int * cache = new int(0);
    bool * b = new bool(true);
    int * x = new int(0);
    int * a = new int(0);
    unsigned long * loop = new unsigned long(0x0ffffffe);
    
    void A() { --*x; *b = false; }
    void B() { ++*x; *b = true; }
    

    两种方法的-O3版本之间几乎没有明显的区别,但是没有-O3,第二种情况确实运行得稍微快一点,至少在我的机器上是这样。我已经测试了没有-O3和循环=0xfffffffe。
    最佳时间:
    alin@ubuntu:~/Desktop$time。 /1

    真实0m20.231s
    用户0m20.224s
    sys 0m0.020s

    alin@ubuntu:~/桌面$time. /2

    真正的0m19.932s
    用户0m19.890
    sys 0m0.060s


  • 共2个答案

    匿名用户

    这有两个部分:

    首先,编译器是否对此进行了优化?

    让我们运行一个实验:

    #include <random>
    #include "test2.h"
    
    int main() {
      std::default_random_engine e;
      std::uniform_int_distribution<int> d(0,1);
      int flag = d(e);
    
      int x = 0;
      int a = 1;
    
      if (flag) {
        x += a;
        doA(x);
        return x;
      } else {
        x += a;
        doB(x);
        return x;
      }
    }
    
    void doA(int& x);
    void doB(int& x);
    
    void doA(int& x) {}
    void doB(int& x) {}
    

    test2.cc和test2. h的存在只是为了防止编译器优化掉所有内容。编译器不能确定没有副作用,因为这些函数存在于另一个翻译单元中。

    现在我们编译为汇编:

    gcc -std=c++11 -S test.cc
    

    让我们跳到组件中有趣的部分:

      call  _ZNSt24uniform_int_distributionIiEclISt26linear_congruential_engineImLm16807ELm0ELm2147483647EEEEiRT_
      movl  %eax, -40(%rbp); <- setting flag
      movl  $0, -44(%rbp);   <- setting x
      movl  $1, -36(%rbp);   <- setting a
      cmpl  $0, -40(%rbp);   <- first part of if (flag)
      je    .L2;             <- second part of if (flag)
      movl  -44(%rbp), %edx  <- setting up x
      movl  -36(%rbp), %eax  <- setting up a
      addl  %edx, %eax       <- adding x and a
      movl  %eax, -44(%rbp)  <- assigning back to x
      leaq  -44(%rbp), %rax  <- grabbing address of x
      movq  %rax, %rdi       <- bookkeeping for function call
      call  _Z3doARi         <- function call doA
      movl  -44(%rbp), %eax
      jmp   .L4
    .L2:
      movl  -44(%rbp), %edx  <- setting up x
      movl  -36(%rbp), %eax  <- setting up a
      addl  %edx, %eax       <- perform the addition
      movl  %eax, -44(%rbp)  <- move it back to x
      leaq  -44(%rbp), %rax  <- and so on
      movq  %rax, %rdi
      call  _Z3doBRi
      movl  -44(%rbp), %eax
    .L4:
    

    所以我们可以看到编译器没有优化它。但我们也没有真正要求它这样做。

    g++ -std=c++11 -S -O3 test.cc
    

    然后是有趣的组装:

    main:
    .LFB4729:
      .cfi_startproc
      subq  $56, %rsp
      .cfi_def_cfa_offset 64
      leaq  32(%rsp), %rdx
      leaq  16(%rsp), %rsi
      movq  $1, 16(%rsp)
      movq  %fs:40, %rax
      movq  %rax, 40(%rsp)
      xorl  %eax, %eax
      movq  %rdx, %rdi
      movl  $0, 32(%rsp)
      movl  $1, 36(%rsp)
      call  _ZNSt24uniform_int_distributionIiEclISt26linear_congruential_engineImLm16807ELm0ELm2147483647EEEEiRT_RKNS0_10param_typeE
      testl %eax, %eax
      movl  $1, 12(%rsp)
      leaq  12(%rsp), %rdi
      jne   .L83
      call  _Z3doBRi
      movl  12(%rsp), %eax
    .L80:
      movq  40(%rsp), %rcx
      xorq  %fs:40, %rcx
      jne   .L84
      addq  $56, %rsp
      .cfi_remember_state
      .cfi_def_cfa_offset 8
      ret
    .L83:
      .cfi_restore_state
      call  _Z3doARi
      movl  12(%rsp), %eax
      jmp   .L80
    

    这有点超出了我清楚地展示程序集和代码之间1对1关系的能力,但是你可以从对doA和doB的调用中看出,设置都是常见的,并且是在if语句之外完成的。(在jne. L83行之上)。所以是的,编译器确实执行了这种优化。

    第2部分:

    如果给出第一个代码,我们如何知道CPU是否进行了这种优化?

    实际上,我不知道有什么方法可以测试这一点。所以我不知道。鉴于存在无序和投机执行,我认为这是合理的。但是证据就在布丁里,我没有办法测试这个布丁。所以我不愿意以这样或那样的方式提出索赔。

    匿名用户

    回到过去,CPU明确支持类似于这样的东西——在分支指令之后,无论是否实际采用分支,都会始终执行下一条指令(查找“分支延迟槽”)。

    我很确定现代CPU只是将整个管道转储到错误预测的分支上。当编译器可以在编译时轻松执行时,尝试在执行时进行您建议的优化是没有意义的。