众所周知,我们可以使用CMOV指令来编写无分支代码,但我想知道我是否在编写x = cond ? 1 : 2的等效代码,我是否更喜欢
x = cond ? 1 : 2
CMOVE rax, 1 #1a CMOVNE rax, 2 #1b
或
MOV rax, 1 #2a CMOVNE rax, 2 #2b
理论上,第一个可以并行执行,而第二个由于数据依赖性而较慢,但我不确定实际情况如何。
pu3pd22g1#
第二个似乎更好。首先,注意CMOVcc没有直接形式;第二个操作数也必须是寄存器(或内存),因此CMOVcc rax, rbx实际上有 * 三 * 个输入依赖性; rax、rbx和flags。rax是输入相关性,因为如果cc为假,则rax中的输出值必须等于它之前的值。因此,指令将始终暂停,直到所有三个输入就绪。你可能会想象一个“条件依赖”,如果cc为真,指令就不需要在rax上停顿,但我不相信任何现有的机器能做到这一点。通常,条件移动被视为算术/逻辑指令,有点类似于adc rax, rbx:它会计算rax,rbx和标志的函数,并将结果保存在rax中,你可以将这个函数想象成rax = (~mask & rax) | (mask & rbx).(This是无分支代码的主要缺点之一:它总是要等待两个结果都准备好。一个分支可能看起来更糟糕,但是如果它被正确预测,那么它只等待真正需要的结果。Linus Torvalds wrote a famous rant about this.)所以第一个例子看起来更像是
CMOVcc
CMOVcc rax, rbx
rax
rbx
flags
cc
adc rax, rbx
rax = (~mask & rax) | (mask & rbx)
mov rbx, 1 mov rcx, 2 cmp whatever cmove rax, rbx cmovne rax, rcx
(我知道我们应该使用32位寄存器来保存雷克斯前缀,但这只是一个例子。)我们现在可以看到几个问题。
cmov
rcx
mov
第二个备选方案如下所示:
mov rax, 1 mov rbx, 2 cmp whatever cmovne rax, rbx
这样就避免了大部分问题。如前所述,在一个实施例中,直接的X1 M28 N1 X到X1 M29 N1 X和X1 M30 N1 X没有输入相关性并且可以预先完成。而且对rax的前一个值没有假依赖,因为mov rax, 1肯定会清除它。最后的好处是,这个版本少了一条指令。
mov rax, 1
1条答案
按热度按时间pu3pd22g1#
第二个似乎更好。
首先,注意
CMOVcc
没有直接形式;第二个操作数也必须是寄存器(或内存),因此CMOVcc rax, rbx
实际上有 * 三 * 个输入依赖性;rax
、rbx
和flags
。rax
是输入相关性,因为如果cc
为假,则rax
中的输出值必须等于它之前的值。因此,指令将始终暂停,直到所有三个输入就绪。你可能会想象一个“条件依赖”,如果
cc
为真,指令就不需要在rax
上停顿,但我不相信任何现有的机器能做到这一点。通常,条件移动被视为算术/逻辑指令,有点类似于adc rax, rbx
:它会计算rax
,rbx
和标志的函数,并将结果保存在rax
中,你可以将这个函数想象成rax = (~mask & rax) | (mask & rbx)
.(This是无分支代码的主要缺点之一:它总是要等待两个结果都准备好。一个分支可能看起来更糟糕,但是如果它被正确预测,那么它只等待真正需要的结果。Linus Torvalds wrote a famous rant about this.)
所以第一个例子看起来更像是
(我知道我们应该使用32位寄存器来保存雷克斯前缀,但这只是一个例子。)
我们现在可以看到几个问题。
cmov
必须等待rbx
和rcx
准备好,但这可能不是问题;直接mov
根本没有输入依赖性,因此它们可能在很久以前就已经乱序执行了。cmov
通过rax
对第一个cmov
的输出具有输入依赖性,因此这两个cmov
实际上必须串行执行,而不是并行执行。cmov
对rax
的前一个值有一个 false 依赖关系。如果一些早期的代码对rax
做了一些缓慢的事情,那么这段代码将暂停,直到另一段代码完成,即使早期代码在rax
中留下的值与这个片段完全无关。第二个备选方案如下所示:
这样就避免了大部分问题。如前所述,在一个实施例中,直接的X1 M28 N1 X到X1 M29 N1 X和X1 M30 N1 X没有输入相关性并且可以预先完成。而且对
rax
的前一个值没有假依赖,因为mov rax, 1
肯定会清除它。最后的好处是,这个版本少了一条指令。