0

我正在阅读关于优化的维基百科页面: http ://en.wikibooks.org/wiki/Optimizing_C%2B%2B/Code_optimization/Pipeline 我遇到了这条线:

对于流水线处理器,比较比差异慢,因为它们意味着一个分支。

为什么比较意味着分支?例如,如果:

int i = 2;
int x = i<5;

这里面有分店吗?对带有条件的 if 语句进行分支是有意义的,但我不明白为什么单独比较会导致分支。

4

4 回答 4

3

这仅涉及一个分支:

unsigned(i – min_i) <= unsigned(max_i – min_i)

虽然这涉及两个:

min_i <= i && i <= max_i

当 CPU 遇到分支时,它会咨询其预测器并遵循最可能的路径。如果预测正确,则分支在性能方面基本上是免费的。如果预测错误,CPU 需要刷新管道并重新开始。

这种优化是一把双刃剑——如果你的分支是高度可预测的,那么第一个实际上可能比第二个运行得慢。这完全取决于您对数据的了解程度。

于 2015-01-14T16:10:03.507 回答
3

序言:现代编译器能够以各种方式消除分支。因此,没有一个示例必然会导致最终(汇编程序或机器)代码中的分支。

那么为什么逻辑基本上暗示着分支呢?

编码

bool check_interval_branch(int const i, int const min_i, int const max_i)
{
  return min_i <= i && i <= max_i;
} 

可以在逻辑上重写为:

bool check_interval_branch(int const i, int const min_i, int const max_i)
{
  if (min_i <= i) 
  { 
    if (i < max_i) return true; 
  }
  return false;
} 

在这里,您显然有两个分支(第二个分支仅在第一个为真时才执行 -短路),分支预测器可能会错误地预测它们,从而导致管道重新滚动。

Visual Studio 2013(优化转一)生成以下程序集,其中包含两个分支check_interval_branch

  push  ebp
  mov   ebp, esp
  mov   eax, DWORD PTR _i$[ebp]
  cmp   DWORD PTR _min_i$[ebp], eax    // comparison
  jg    SHORT $LN3@check_inte          // conditional jump
  cmp   eax, DWORD PTR _max_i$[ebp]    // comparison
  jg    SHORT $LN3@check_inte          // conditional jump
  mov   al, 1
  pop   ebp
  ret   0
$LN3@check_inte:
  xor   al, al
  pop   ebp
  ret   0

编码

bool check_interval_diff(int const i, int const min_i, int const max_i)
{
  return unsigned(i - min_i) <= unsigned(max_i - min_i);
}

逻辑上等同于

bool check_interval_diff(int const i, int const min_i, int const max_i)
{
  if (unsigned(i – min_i) <= unsigned(max_i – min_i)) { return true; }
  return false;
}

它只包含一个分支,但执行两个差异。

Visual Studio 2013的生成代码check_interval_diff甚至不包含条件跳转。

  push  ebp
  mov   ebp, esp
  mov   edx, DWORD PTR _i$[ebp]
  mov   eax, DWORD PTR _max_i$[ebp]
  sub   eax, DWORD PTR _min_i$[ebp]
  sub   edx, DWORD PTR _min_i$[ebp]
  cmp   eax, edx                    // comparison
  sbb   eax, eax
  inc   eax
  pop   ebp
  ret   0

(这里的技巧是,根据进位标志,由指令完成的减法sbb相差 1,而进位标志又被cmp指令设置为 1 或 0。)

实际上,您在这里看到三个不同之处(2x sub, 1x sbb)。

这可能取决于您的数据/用例哪个更快。

请参阅此处有关分支预测的Mysticals 答案。

您的代码int x = i<5;在逻辑上与

int x = false;
if (i < 5)
{
  x = true;
}

再次包含一个分支(x = true仅在i < 5. 时执行)

于 2015-01-14T16:40:22.443 回答
3

虽然这里给出的答案很好,但并非所有比较都被转换为分支指令(它们确实引入了数据依赖性,这也可能会降低您的性能)。

例如下面的 C 代码

int main()
{
    volatile int i;
    int x = i<5;

    return x;
}

由 gcc (x86-64, Optimizations enabled) 编译成:

    movl    -4(%rbp), %eax
    cmpl    $5, %eax
    setl    %al
    movzbl  %al, %eax

该指令根据其前面的比较指令的结果setl设置 的值。AL

当然,这是一个非常简单的示例——这种cmp/setl组合可能会引入依赖关系,从而阻止处理器并行执行它们,甚至可能会花费您几个周期。

尽管如此,在现代处理器上,并不是所有的比较都会变成分支指令。

于 2015-01-14T16:56:14.280 回答
1

曾经写过该页面的人不具备程序员的能力。首先,比较不一定意味着分支;这取决于你对他们做了什么。这是否意味着分支取决于处理器和编译器。Anif通常需要一个分支,但即便如此,一个好的优化器有时也可以避免它。Awhile或 a for通常需要一个分支,除非编译器可以展开循环,但该分支是高度可预测的,因此即使分支预测是一个问题,也可能无关紧要。

更一般地说,如果您在编写代码时担心这个级别的任何事情,那么您就是在浪费时间,并使维护变得更加困难。您唯一应该担心的是一旦您遇到性能问题,并且分析器显示这是您失去性能的地方。此时,您可以尝试几种不同的代码编写方式,以确定哪一种可以为您的编译器和硬件组合带来更快的代码。(更改编译器或硬件,可能不一样。)

于 2015-01-14T18:06:37.373 回答