在阅读了这篇文章(在 StackOverflow 上的答案)(在优化部分)之后,我想知道为什么条件移动不容易受到分支预测失败的影响。我在这里找到了一篇关于 cond move 的文章(AMD 提供的 PDF)。同样在那里,他们声称 cond 的性能优势。移动。但这是为什么呢?我没看到。在评估该 ASM 指令时,尚不知道前面的 CMP 指令的结果。
5 回答
错误预测的分支很昂贵
如果一切顺利的话,现代处理器通常每个周期执行 1 到 3 条指令(如果它没有停止等待这些指令从先前指令或内存到达的数据依赖关系)。
上面的语句非常适用于紧密循环,但这不应该让您忽视一个额外的依赖关系,它可以阻止指令在其循环到来时被执行:对于要执行的指令,处理器必须已经开始获取和解码它在 15-20 个周期之前。
处理器遇到分支应该怎么做?获取和解码两个目标不会扩展(如果有更多分支,则必须并行获取指数数量的路径)。因此,推测性地,处理器仅获取和解码两个分支之一。
这就是错误预测分支代价高昂的原因:它们花费了 15-20 个周期,而这些周期通常是不可见的,因为高效的指令流水线。
有条件的搬家从来都不是很贵
有条件的移动不需要预测,所以它永远不会有这个惩罚。它具有数据依赖性,与普通指令相同。事实上,条件移动比普通指令有更多的数据依赖,因为数据依赖包括“条件真”和“条件假”两种情况。r1
在有条件地移动到的指令之后r2
, 的内容r2
似乎取决于 和 的先前r2
值r1
。良好预测的条件分支允许处理器推断更准确的依赖关系。但是数据依赖关系通常需要一到两个周期才能到达,如果它们需要时间来到达的话。
请注意,从内存到寄存器的有条件移动有时是一个危险的赌注:如果条件是从内存读取的值没有分配给寄存器,那么您在内存上等待是徒劳的。但是指令集中提供的条件移动指令通常是寄存器到寄存器,防止程序员出现这种错误。
这都是关于指令流水线的。请记住,现代 CPU 在流水线中运行它们的指令,当 CPU 可预测执行流程时,这会显着提升性能。
cmov
add eax, ebx
cmp eax, 0x10
cmovne ebx, ecx
add eax, ecx
在评估该 ASM 指令时,尚不知道前面的 CMP 指令的结果。
也许吧,但是 CPU 仍然知道,无论and指令cmov
的结果如何,紧随其后的指令都会被立即执行。因此,可以提前安全地获取/解码下一条指令,而分支并非如此。cmp
cmov
下一条指令甚至可以在执行之前执行cmov
(在我的示例中这是安全的)
分支
add eax, ebx
cmp eax, 0x10
je .skip
mov ebx, ecx
.skip:
add eax, ecx
在这种情况下,当 CPU 的解码器看到je .skip
它时,它必须选择是 1) 从下一条指令继续预取/解码指令,还是 2) 从跳转目标开始。CPU 会猜测这个前向条件分支不会发生,因此下一条指令mov ebx, ecx
将进入流水线。
几个周期后,je .skip
执行并采用分支。哦,废话!我们的管道现在包含一些不应该执行的随机垃圾。CPU 必须刷新所有缓存的指令并从.skip:
.
cmov
这是错误预测分支的性能损失,因为它不会改变执行流程,所以永远不会发生这种情况。
实际上结果可能还不知道,但如果其他情况允许(特别是依赖链),cpu 可以重新排序并执行cmov
. 由于不涉及分支,因此在任何情况下都需要评估这些指令。
考虑这个例子:
cmoveq edx, eax
add ecx, ebx
mov eax, [ecx]
后面的两条指令cmov
不依赖于 的结果cmov
,因此即使在cmov
自身处于挂起状态时也可以执行它们(这称为乱序执行)。即使它们不能被执行,它们仍然可以被获取和解码。
分支版本可以是:
jne skip
mov edx, eax
skip:
add ecx, ebx
mov eax, [ecx]
这里的问题是控制流正在发生变化,并且 cpu 不够聪明,无法看到mov
如果分支被错误预测为已被占用,它可以“插入”跳过的指令 - 相反,它会丢弃分支后所做的一切,并重新启动从头开始。这就是惩罚的来源。
你应该阅读这些。使用 Fog+Intel,只需搜索 CMOV。
大约 2007 年,Linus Torvald 对 CMOV 的评论
Agner Fog 对微架构的比较
英特尔® 64 和 IA-32 架构优化参考手册
简短的回答,正确的预测是“免费的”,而条件分支错误预测可能会在 Haswell 上花费 14-20 个周期。但是,CMOV 从来都不是免费的。我仍然认为 CMOV 现在比 Torvalds 咆哮时好多了。在所有处理器上,没有一个永远正确的答案。