在汇编中,无分支代码是否应该使用互补的 CMOV?

Dan*_*iel 6 x86 assembly micro-optimization conditional-move branchless

众所周知,我们可以使用 CMOV 指令来编写无分支代码,但我想知道我是否正在编写等效的指令x = cond ? 1 : 2,我应该更喜欢

CMOVE rax, 1    #1a
CMOVNE rax, 2   #1b
Run Code Online (Sandbox Code Playgroud)

或者

MOV rax, 1      #2a
CMOVNE rax, 2   #2b
Run Code Online (Sandbox Code Playgroud)

理论上,第一个可以几乎并行执行,而第二个由于数据依赖性而速度较慢。但我不确定现实情况如何。

Nat*_*dge 10

第二个似乎更好。

首先,请注意CMOVcc没有直接形式;第二个操作数也必须是寄存器(或内存)。因此CMOVcc rax, rbx实际上具有三个输入依赖项;raxrbx, 和flagsrax是输入依赖项,因为如果cc为 false,则 in 的输出值rax必须等于之前的值。因此,指令将始终停止,直到所有三个输入都准备好为止。

rax您可能会想象一种“条件依赖”,其中如果为真,指令不需要停止cc,但我不相信任何当前现有的机器实际上可以做到这一点。一般来说,条件移动被视为算术/逻辑指令,有点类似于adc rax, rbx:它计算 的某个函数raxrbx标志,并将结果保留在 中rax。您可以将该函数视为类似rax = (~mask & rax) | (mask & rbx).

(这是无分支代码的主要缺点之一:它总是必须等待两个结果都准备好。分支可能看起来更糟糕,但如果它被正确预测,那么它只等待实际需要的结果 。Linus托瓦尔兹对此写了一篇著名的咆哮。

所以第一个例子看起来更完整

mov rbx, 1
mov rcx, 2
cmp whatever
cmove rax, rbx
cmovne rax, rcx
Run Code Online (Sandbox Code Playgroud)

(我知道我们应该使用 32 位寄存器来保存 REX 前缀,但这只是一个示例。)

我们现在可以看到几个问题。

  • scmov必须等待rbxrcx做好准备,但这可能不是问题;立即mov数根本没有输入依赖性,因此它们很可能早就乱序执行了。

  • 更严重的是,第二个cmov输入依赖于第一个输出,通过rax。所以这两个cmov实际上必须串行执行,而不是并行执行。

  • 更糟糕的是,第一个错误地cmov依赖于先前的值was。如果某些较早的代码正在执行缓慢的操作,则此代码将停止,直到其他代码完成,即使较早的代码留下的值应该与此代码段完全无关。raxraxrax

第二种选择如下:

mov rax, 1
mov rbx, 2
cmp whatever
cmovne rax, rbx
Run Code Online (Sandbox Code Playgroud)

这可以避免大部分问题。现在唯一真正的输入依赖是由 生成的标志cmp(因此无论输入是什么cmp)。和以前一样,立即数movs toraxrbx没有输入依赖性,可以提前完成。并且不存在对先前值的错误依赖,rax因为mov rax, 1最终将其消除。作为最后的奖励,这个版本缩短了一条指令。