当我编写一些需要快速处理的紧凑循环时,我经常会考虑处理器的分支预测是如何运作的。例如,我尽力避免在最内层循环中使用if语句,特别是那些结果不太均匀的if语句(比如随机评估为true或false)。
我这样做,是因为有一个比较普遍的观点,即处理器预取指令,如果它预测错误分支,那么预取就是无用的。
我的问题是:这真的是现代处理器的问题吗?分支预测能有多好呢?
什么编码模式可以使其更好?
(为了讨论的方便,假设我已经超越了“过早优化是万恶之源”的阶段)
当我编写一些需要快速处理的紧凑循环时,我经常会考虑处理器的分支预测是如何运作的。例如,我尽力避免在最内层循环中使用if语句,特别是那些结果不太均匀的if语句(比如随机评估为true或false)。
我这样做,是因为有一个比较普遍的观点,即处理器预取指令,如果它预测错误分支,那么预取就是无用的。
我的问题是:这真的是现代处理器的问题吗?分支预测能有多好呢?
什么编码模式可以使其更好?
(为了讨论的方便,假设我已经超越了“过早优化是万恶之源”的阶段)
分支预测现在相当不错。但这并不意味着可以消除分支的惩罚。
在典型代码中,您可能会得到超过99%的正确预测,但性能损失仍然可能很大。其中有几个因素起作用。
一个因素是简单的分支延迟。在普通PC CPU上,错误预测的成本可能在12个周期左右,而正确预测的分支只需要1个周期。为了论证问题,假设所有分支都被正确预测,那么就成功了吗?不完全是。
有分支的简单存在会抑制很多优化。编译器无法在分支之间有效地重组代码。在基本块(即按顺序执行、没有分支、有一个入口点和一个出口点的代码块)内,它可以随意重新排列指令,只要代码的含义得以保留,因为所有指令最终都会被执行。在跨越分支时,情况就变得棘手了。我们可以将这些指令移动到此分支之后执行,但是如何保证它们被执行呢?将它们放在两个分支中?这增加了代码大小,也很混乱,并且如果我们想在多个分支之间重新排序,它就不可扩展了。
即使使用最佳的分支预测,分支仍然可能很昂贵。这不仅是因为错误预测,还因为指令调度变得更加困难。
这也意味着重要的因素不是分支的数量,而是它们之间的代码量。每隔一行就有一个分支是不好的,但如果你能在分支之间放入一打行代码,可能可以将这些指令合理地安排到调度中,使得分支不会过于限制CPU或编译器。
但在典型代码中,分支基本上是免费的。在典型代码中,没有那么多紧密聚集在性能关键代码中的分支。
如果我们已经超过了“早期优化”阶段,那么我们肯定也超过了“我可以测量它”的阶段吧?由于现代CPU架构的复杂性,唯一确定的方法就是尝试并测量。当然不可能有很多情况下你会有两种实现方式的选择,其中一种需要分支而另一种不需要。
我的答案是:
AMD在过去有时比英特尔更快或更好的原因很简单,那就是他们拥有更好的分支预测。
如果您的代码没有分支预测(即它没有分支),则可以预期其运行速度更快。
因此,结论是:如果不必要,请避免使用分支。如果需要,请尝试使一个分支评估95%的时间。
我最近在TI DSP上发现的一件事是,尝试避免分支有时会生成比分支预测成本更多的代码。
我在一个紧密循环中有类似以下的东西:
if (var >= limit) { otherVar = 0;}
我想要消除潜在的分支,并尝试将其更改为:
otherVar *= (var<limit)&1;
但是“优化”生成的汇编代码是原来的两倍,并且实际上更慢。
var<limit
包含一个隐含的分支,而&1
是无用的。 - Matteo Italia