有时CPU占用大部分时间的循环经常会出现一些分支预测错误(错误预测)(接近0.5概率)。我在非常孤立的线程上看到了一些技术但从未列出过。我所知道的那些已经解决了条件可以变为bool并且以某种方式使用0/1来改变的情况。是否有其他可以避免的条件分支?
e.g。 (伪代码)
loop () {
if (in[i] < C )
out[o++] = in[i++]
...
}
可以用这样的东西重写,可能会失去一些可读性:
loop() {
out[o] = in[i] // copy anyway, just don't increment
inc = in[i] < C // increment counters? (0 or 1)
o += inc
i += inc
}
此外,我已经看到了在某些情况下,在条件允许的情况下将&&
更改为&
的野外技巧。我是这个优化级别的新手,但确实感觉还有更多。
答案 0 :(得分:13)
使用Matt Joiner的例子:
if (b > a) b = a;
您也可以执行以下操作,而无需深入了解汇编代码:
bool if_else = b > a;
b = a * if_else + b * !if_else;
答案 1 :(得分:11)
我认为避免分支的最常用方法是利用位并行来减少代码中的总跳转。基本块越长,管道刷新的频率就越低。
正如其他人所提到的,如果你想做的不仅仅是展开循环,并提供分支提示,你还是想要进入程序集。当然,这应该非常谨慎地进行:在大多数情况下,典型的编译器可以编写比人类更好的汇编。你最大的希望是削减粗糙的边缘,并做出编译器无法推断的假设。
以下是以下C代码的示例:
if (b > a) b = a;
在没有任何跳跃的程序集中,通过使用位操作(和极端注释):
sub eax, ebx ; = a - b
sbb edx, edx ; = (b > a) ? 0xFFFFFFFF : 0
and edx, eax ; = (b > a) ? a - b : 0
add ebx, edx ; b = (b > a) ? b + (a - b) : b + 0
请注意,虽然组件爱好者会立即跳过条件移动,但这只是因为它们易于理解并在方便的单个指令中提供更高级别的语言概念。它们不一定更快,在旧处理器上不可用,并且通过将C代码映射到相应的条件移动指令,您只是在编译器的工作。
答案 2 :(得分:8)
您给出的示例的概括是“用数学替换条件评估”;条件分支避免很大程度上归结为那个。
将&&
替换为&
后发生的事情是,由于&&
是短路的,因此它本身构成了条件评估。如果双方都是0或1,并且不是短路,&
会得到相同的逻辑结果。这同样适用于||
和|
,除非您不需要确保边被约束为0或1(再次,仅出于逻辑目的,即您仅使用布尔结果)。
答案 3 :(得分:4)
GCC已经足够聪明,可以用更简单的指令替换条件。例如,较新的英特尔处理器提供cmov(条件移动)。如果您可以使用它,SSE2一次向compare 4 integers(或8个短路,或16个字符)提供一些指令。
另外,要计算最小值,您可以使用(请参阅这些magic tricks):
min(x, y) = x+(((y-x)>>(WORDBITS-1))&(y-x))
但是,请注意以下事项:
c[i][j] = min(c[i][j], c[i][k] + c[j][k]); // from Floyd-Warshal algorithm
即使没有隐含的跳跃比
慢得多int tmp = c[i][k] + c[j][k];
if (tmp < c[i][j])
c[i][j] = tmp;
我最好的猜测是,在第一个片段中,您会更频繁地污染缓存,而在第二个片段中则不会。
答案 4 :(得分:4)
在这个级别,事情依赖于硬件和编译器。您使用的编译器是否足够智能以编译&lt;没有控制流? x86上的gcc足够聪明; lcc不是。在旧的或嵌入式指令集上,可能无法计算&lt;没有控制流程。
除了类似卡桑德拉的警告之外,很难做出任何有用的一般性陈述。所以这里有一些可能无益的一般性陈述:
现代分支预测硬件非常好。如果你能找到一个真正的程序,其中坏分支预测的成本降幅超过1%-2%,我会非常惊讶。
告诉您在哪里可以找到分支误预测的性能计数器或其他工具是不可或缺的。
如果你真的需要改进这样的代码,我会研究跟踪调度和循环展开:
循环展开会复制循环体并为优化程序提供更多控制流程。
跟踪调度识别最可能采用的路径,以及其他技巧,它可以调整分支方向,以便分支预测硬件在最常见的路径上更好地工作。对于展开的循环,路径越来越长,因此跟踪调度程序可以使用更多
我很想在集会中尝试自己编码。当下一个芯片推出新的分支预测硬件时,很有可能你所有的辛勤工作都耗尽了。相反,我会寻找一个反馈导向的优化编译器。
答案 5 :(得分:2)
在我看来,如果你达到这种优化水平,可能是时候直接进入汇编语言了。
基本上,你依靠编译器生成一个特定的程序集模式,无论如何都要利用C中的这种优化。很难准确猜出编译器将生成什么代码,所以你必须在任何时候进行一些小改动时再看看它 - 为什么不在装配中做它并用它来完成呢?
答案 6 :(得分:2)
大多数处理器提供的分支预测优于50%。事实上,如果你的分支预测得到1%的提高,那么你可以发表一篇论文。如果你感兴趣的话,有很多关于这个主题的论文。
最好不要担心缓存命中和遗漏。
答案 7 :(得分:2)
当您必须执行多个嵌套测试以获得答案时,原始问题中演示的技术的扩展适用。您可以根据所有测试的结果构建一个小位掩码,并在表格中“查找”答案。
if (a) {
if (b) {
result = q;
} else {
result = r;
}
} else {
if (b) {
result = s;
} else {
result = t;
}
}
如果a和b几乎是随机的(例如,来自任意数据),并且这是一个紧密的循环,那么分支预测失败可以真正减慢它。可以写成:
// assuming a and b are bools and thus exactly 0 or 1 ...
static const table[] = { t, s, r, q };
unsigned index = (a << 1) | b;
result = table[index];
您可以将此概括为几个条件。我已经看到它已经完成了4次。但是,如果嵌套深入,你想确保测试所有这些测试真的比仅进行短路评估建议的最小测试要快。
答案 8 :(得分:1)
除了最热门的热点之外,这种优化水平不太可能产生有价值的差异。假设它(在特定情况下没有证明)是一种猜测的形式,并且优化的第一条规则是不作用于猜测。