提问人:aaron-prindle 提问时间:1/15/2015 最后编辑:user16217248aaron-prindle 更新时间:11/13/2023 访问量:652
比较是否意味着一个分支?
Is it the case that comparisons imply a branch?
问:
我正在阅读关于优化的维基教科书页面,我遇到了这样一句话:
对于流水线处理器,比较比差异慢,因为它们意味着分支。
为什么比较意味着一个分支? 例如,如果:
int i = 2;
int x = i<5;
这里面有分支吗?使用条件对 if 语句进行分支对我来说是有意义的,但我不明白为什么单独的比较会导致分支。
答:
这仅涉及一个分支:
unsigned(i – min_i) <= unsigned(max_i – min_i)
虽然这涉及两个:
min_i <= i && i <= max_i
当 CPU 遇到分支时,它会咨询其预测器并遵循最可能的路径。如果预测是正确的,则该分支在性能方面基本上是免费的。如果预测错误,CPU 需要刷新管道并重新开始。
这种优化是一把双刃剑---如果你的分支是高度可预测的,那么第一个分支实际上可能比第二个运行得慢。这完全取决于您对数据的了解程度。
评论
bool
if
min_i <= i && i <= max_i
序言:现代编译器能够以各种方式消除分支。因此,没有一个示例必然会在最终(汇编程序或机器)代码中产生分支。
那么,为什么逻辑基本上意味着分支呢?
代码
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。sbb
cmp
事实上,你在这里看到了三个不同之处(2x,1x)。sub
sbb
这可能取决于您的数据/用例,哪一个更快。
请参阅此处关于分支预测的 Mysticals 答案。
您的代码在逻辑上与int x = i<5;
int x = false;
if (i < 5)
{
x = true;
}
它再次包含一个分支(仅在以下情况下执行。x = true
i < 5
评论
int x = i < 5;
condition
condition ? true : false
if
虽然这里给出的答案很好,但并非所有比较都转化为分支指令(它们确实引入了数据依赖关系,这也可能会降低一些性能)。
例如,以下 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
该指令根据其前面的比较指令的结果设置 的值。setl
AL
当然,这是一个非常简单的例子 - 这种组合可能会引入依赖关系,阻止处理器并行执行它们,甚至可能花费你几个周期。cmp/setl
尽管如此,在现代处理器上,并非所有比较都变成了分支指令。
评论
setl
sbb %al,%al
谁曾经写过那个页面,谁就不是一个合格的程序员。第一
比较并不一定意味着一个分支;这取决于你什么
和他们一起做。这是否意味着一个分支取决于
处理器和编译器。通常需要一个分支,但是
即便如此,一个好的优化器有时也可以避免它。A 或 A 通常需要一个分支,除非编译器可以展开
循环,但该分支是高度可预测的,因此即使分支
预测是一个问题,可能无关紧要。if
while
for
更一般地说,如果您在写作时担心这个级别的任何事情 你的代码,你浪费了你的时间,并使维护工作变得更加多 难。你唯一应该担心的时候是一旦你有一个 性能问题,并且探查器显示这是 你正在失去性能。此时,您可以尝试 编写代码的几种不同方法,以确定哪种方式将 为编译器和硬件的组合提供更快的代码。 (更改编译器或硬件,它们可能不是同一个。
上一个:了解基本动态分配示例
下一个:一个好的设计可以完全避免铸件吗?
评论