当我写一些需要快速工作的紧密循环时,我常常被关于处理器分支预测将如何表现的想法所困扰。例如,我尽量避免在最内部循环中使用if语句,尤其是结果不一致的结果(比如判断为true或false随机)。
我倾向于这样做,因为处理器预先获取指令有一些常见的知识,如果事实证明它错误地预测了一个分支,那么预取是没用的。
我的问题是 - 这真的是现代处理器的问题吗?预期分支预测有多好? 可以使用哪些编码模式使其更好?
(为了讨论,假设我超越了“早期优化是所有邪恶的根源”阶段)
答案 0 :(得分:22)
这些天分支预测非常好。但这并不意味着可以消除分支的惩罚。
在典型代码中,您可能会获得超过99%的正确预测,但性能损失仍然很大。这有几个因素在起作用。
一个是简单的分支延迟。在普通的PC CPU上,错误预测可能大约为12个周期,正确预测的分支可能为1个周期。为了争论,让我们假设你的所有分支都被正确预测,然后你就可以自由了,对吗?不太好。
分支的简单存在抑制了许多优化。 编译器无法跨分支有效地重新排序代码。在基本块(即,顺序执行的代码块,没有分支,一个入口点和一个出口)中,只要保留代码的含义,它就可以根据需要重新排序指令,因为它们是所有人迟早都会被处决。跨越分支机构,它变得更加棘手。我们可以将这些指令移到该分支后执行,但是我们如何保证它们被执行?把它们放在两个分支?这是额外的代码大小,也是混乱的,如果我们想要在多个分支上重新排序,它就无法扩展。
即使有最好的分支预测,分支仍然很昂贵。不仅仅是因为误预测,还因为指令调度变得更加困难。
这也意味着,而不是分支的数量,重要的因素是它们之间的块中有多少代码。每隔一行的分支都很糟糕,但是如果你可以在分支之间的一个块中获得十几行,那么很可能可以很好地调整这些指令,因此分支不会过多限制CPU或编译器。
但在典型的代码中,分支基本上是免费的。在典型的代码中,没有 许多分支在性能关键代码中紧密地聚集在一起。
答案 1 :(得分:3)
如果我们超出“早期优化”阶段,那么我们肯定超出了“我可以衡量那个”阶段吗?由于现代CPU架构的疯狂复杂性,唯一可以确定的方法是尝试和测量。当然,在很多情况下,您可以选择两种方式来实现某些方法,其中一种方法需要分支,而另一种方法则不需要。
答案 2 :(得分:3)
“(为了讨论,假设我超越了”早期优化是所有邪恶的根源“阶段)”
优异。然后,您可以分析应用程序的性能,使用gcc的标签再次进行预测和分析,使用gcc的标签再次进行相反的预测和分析。
现在从理论上想象一个预取两个分支路径的CPU。对于两个路径中的后续if语句,它将预取四个路径等.CPU不会神奇地增长四倍的缓存空间,因此它将预取每个路径的一小部分,而不是单个路径。
如果你发现有一半的预取被浪费了,丢失了占你CPU时间的5%,那么你确实想找一个不分支的解决方案。
答案 3 :(得分:2)
不完全是答案,但您可以在这里找到applet demonstrates the finite state machine often used for table-based branch-prediction in modern microprocessors。
它说明了使用额外的逻辑来为分支条件和目标地址生成快速(但可能是错误的)估计 处理器以全速获取并执行预测指令,但需要在预测结果出错时还原所有中间结果。
答案 4 :(得分:2)
是的,分支预测确实可以成为性能问题。
This question(目前StackOverflow上投票率最高的问题)举了一个例子。
答案 5 :(得分:1)
我的回答是:
AMD在某些方面与英特尔一样快或更好的原因是过去只是他们有更好的分支预测。
如果你的代码没有分支预测,(意思是它没有分支),那么它可以运行得更快。
因此,结论:如果没有必要,请避免使用分支。如果是,请尝试使其在95%的时间内评估一个分支。
答案 6 :(得分:0)
我最近发现的一件事(在TI DSP上)是试图避免分支有时会产生比分支预测成本更多的代码。
我在紧密的循环中有类似的内容:
if (var >= limit) { otherVar = 0;}
我想摆脱潜在的分支,并尝试将其更改为:
otherVar *= (var<limit)&1;
但'优化'产生的装配量是装配量的两倍,实际上更慢。