比较是否意味着一个分支?

Is it the case that comparisons imply a branch?

提问人:aaron-prindle 提问时间:1/15/2015 最后编辑:user16217248aaron-prindle 更新时间:11/13/2023 访问量:652

问:

我正在阅读关于优化的维基教科书页面,我遇到了这样一句话:

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

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

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

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

C++ 优化 流水线无 分支

评论

3赞 siritinga 1/15/2015
我认为这是因为比较(以及后续分支)会影响分支预测变量 en.wikipedia.org/wiki/Branch_predictor。当预测良好时,分支预测器会加快流水线处理器中的执行速度,并在失败时减慢执行速度(必须再次填充流水线)。

答:

3赞 Cory Nelson 1/15/2015 #1

这仅涉及一个分支:

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

虽然这涉及两个:

min_i <= i && i <= max_i

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

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

评论

0赞 James Kanze 1/15/2015
两者都不一定涉及分支;编译器可以很容易地优化其中任何一个,以消除所有分支,至少在大多数架构上是这样。
0赞 Cory Nelson 1/15/2015
同意,取决于它的使用方式,这没有具体说明。
0赞 James Kanze 1/15/2015
是的。如果不使用结果,编译器可能会完全消除测试。如果将它们分配给 a(稍后使用),则在这两种情况下避免分支将是一个相对微不足道的优化。即使他们控制了 ,根据受控块中的内容,也可以避免分支(但这可能需要相当复杂的编译器)。请注意,这没有副作用,因此编译器实际上不必短路,这可以很容易地消除第二个表达式中对额外分支的需求。boolifmin_i <= i && i <= max_i
3赞 Pixelchemist 1/15/2015 #2

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

那么,为什么逻辑基本上意味着分支呢?

代码

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

(这里的诀窍是,根据进位标志,完成的减法相差 1,进位标志又被指令设置为 1 或 0。sbbcmp

事实上,你在这里看到了三个不同之处(2x,1x)。subsbb

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

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

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

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

它再次包含一个分支(仅在以下情况下执行。x = truei < 5

评论

1赞 James Kanze 1/15/2015
即使没有优化,它也将是一个非常糟糕的编译器,它为 生成了一个分支指令。int x = i < 5;
0赞 Pixelchemist 1/15/2015
@JamesKanze我认为问题更多的是关于示例表达式的逻辑,而不是编译器生成的代码是否会以任何方式省略分支的问题。我倾向于说:比较意味着一个分支,在某些(或许多)情况下,可以通过体面的编译器进行优化。
0赞 user16217248 11/13/2023
我不明白这一点。等效于 或带有语句的变体(包含分支)的参数可以多次递归任意应用。仅凭这一点并不能回答这个问题。conditioncondition ? true : falseif
3赞 nimrodm 1/15/2015 #3

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

例如,以下 C 代码

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

    return x;
}

由 gcc(x86-64,启用优化)编译为:

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

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

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

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

评论

0赞 Pixelchemist 1/15/2015
我认为编译器(不是处理器)是权威的,比较变成了一个分支,但总的来说你是对的。在某些情况下,即使逻辑包含分支,优化编译器也会生成无分支代码。
1赞 James Kanze 1/15/2015
@Pixelchemist我认为他的观点是,大多数现代处理器都有指令(如 ),可用于避免分支。当然,即使没有这个,也可以使用诸如此类的东西将进位放入寄存器中。(早在 1980 年,8086 的原始英特尔 PL/M 编译器就这样做了,因此现代编译器似乎并不抱太大的期望。setlsbb %al,%al
1赞 James Kanze 1/15/2015 #4

谁曾经写过那个页面,谁就不是一个合格的程序员。第一 比较不一定意味着一个分支;这取决于你什么 和他们一起做。这是否意味着一个分支取决于 处理器和编译器。通常需要一个分支,但是 即便如此,一个好的优化器有时也可以避免它。A 或 A 通常需要一个分支,除非编译器可以展开 循环,但该分支是高度可预测的,因此即使分支 预测是一个问题,可能无关紧要。ifwhilefor

更一般地说,如果您在写作时担心这个级别的任何事情 你的代码,你浪费了你的时间,并使维护工作变得更加多 难。你唯一应该担心的时候是一旦你有一个 性能问题,并且探查器显示这是 你正在失去性能。此时,您可以尝试 编写代码的几种不同方法,以确定哪种方式将 为编译器和硬件的组合提供更快的代码。 (更改编译器或硬件,它们可能不是同一个。