【问题标题】:Branch Prediction at no cost?免费进行分支预测?
【发布时间】:2015-12-24 17:33:45
【问题描述】:

我刚刚偶然发现了这个东西,我真的很好奇现代 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 在流水线指令(获取、解码、执行等)方面完全不同:

  1. 第二种方法会更快吗?

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

注意:

在第一种情况下,CPU 没有选择,只能丢弃 do A 的前几个管道阶段,如果发生分支未命中预测,则执行 B,因为它们是不同的。我将第二个示例视为某种延迟的分支,例如: " 我将检查该标志,即使我不知道该标志,我也可以继续执行下一条指令,因为它是相同的,无论如何标志是什么,我已经有了下一条指令,我可以使用它。”

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

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

案例 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 和循环 = 0xffffffffe 的情况下进行了测试。
最佳时间:
alin@ubuntu:~/Desktop$ time ./1

真正的 0m20.231s
用户 0m20.224s
系统 0m0.020s

alin@ubuntu:~/Desktop$ 时间./2

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

【问题讨论】:

  • 这样的东西一般是由编译器优化的,而不是在执行/CPU级别。
  • 我怀疑编译器优化器会完成它的工作并将其分解以产生相同的代码。
  • PS:感谢您的代码编辑(这是我的第一篇文章,对此感到抱歉)。所以换句话说,我可以把 c​​ase 2 写成 1 并相信编译器会注意到这一点?
  • @Calvin 排除通用代码会导致优化尝试失败。
  • @AlinIonutLipan:我还没有看到 x86 机器上的编译器这样做(将案例 1 转换为案例 2),但我在几十年前在 RISC 机器上已经看到过瘦(但是不完全像这样。)这确实是由编译器完成的。一般来说,你不能过多的依赖编译器优化,但是这个是比较简单和明显的针孔优化。我建议总是写 case 1,因为编译器更容易做到。

标签: c++ c pipeline branch-prediction


【解决方案1】:

过去,CPU 明确支持有点像这样的东西 - 在分支指令之后,无论是否实际采用分支,下一条指令总是会被执行(查找“分支延迟槽”)。

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

【讨论】:

  • 啊,我只是想记住“延迟槽”这个名称,以便发布与您的几乎完全相同的答案。 :D
  • 谢谢,我不知道延迟槽,这似乎正是我缺少的信息 :) 所以我认为写不洁案例 2 没有意义。
  • 写下任何情况下最清楚的——通常是 1。
【解决方案2】:

这有两个部分:

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

我们来做一个实验:

test.cc

#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;
  }
}

test2.h

void doA(int& x);
void doB(int& x);

test2.cc

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 是否进行了这种优化?

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

【讨论】:

  • 用等效的 C 代码进行相同的解释会更容易混淆。
  • 唯一真正的区别是缺少名称修饰和不同的随机函数名称调用。这很好海事组织。在这两种情况下,我都跳过了大部分设置。
  • 感谢您的回答,是的,我明白我们应该始终不慌不忙地写案例 1。我想知道案例 2 是否有可能比案例 1 更快(假设编译器对这些值一无所知,假设我们到处都有指针,编译器还不知道副作用)。在不知道他怎么可能优化案例 1 的情况下?我要自己做一些测试,看看情况 2 是否可以更快,如果可以,提高多少。
  • 我只测试了案例 2,以表明它可以编译为与案例 1 语义等效的内容。在您给出的有限示例中,我看不出案例 2 可能比案例更快1(仅等于)。或许你可以提供更多细节?
  • 这就是我的意思,名字修饰并且让非 C++ 程序员感到困惑,问题也被标记为 C,flag = rand(); 会很简单。
猜你喜欢
  • 2014-04-25
  • 2014-03-03
  • 2020-06-01
  • 2016-07-01
  • 2011-02-01
  • 2015-11-24
  • 1970-01-01
  • 2012-07-02
  • 2011-07-20
相关资源
最近更新 更多