我有下一个ASM代码:
mov r10 , 9007199254740990 ; mask
mov r8 , rax
shr r8 , 53
sub r8 , 1023
cmp r8 , 52 ; r8 - 52 < 0
setnb ch
shrx r11 , r10 , r8
and r11 , rax
setne cl ; r11 == 0
test rcx , rcx
jz @C_2
ret
@C_2: ; integer
ret
好吧,这里我们只有一个分支指令。我们可以通过替换相应 Jump 指令上的 SETcc 指令来重写这段代码,因此我们将在上面的代码中获得两个分支指令。我的问题是,在普通(随机数据)情况下,哪些代码会运行得更快,为什么?
我假设在该ret
之前的jz
之后有一些实际代码; 在您的示例中,跌倒和采用的路径都会导致ret
。 (这也可能是相同的ret
,无需复制。
您花费了大量额外的指令来无分支地评估单个布尔值。 确保将其与使用2个分支的简单版本进行基准测试,作为完整程序的一部分,在真实的数据模式上。
现代 TAGE 分支预测器使用以前的分支历史记录(沿执行路径)为当前分支的预测编制索引。 您可能会发现 2 分支方式仍然可以很好地预测,并且不会通过污染更多条目来过度损害其他分支的预测率。
对有分支与无分支进行微基准测试是困难的,因为现代预测器非常复杂,以至于它可以对提供给它的代码产生很大的影响。 在重复循环中隔离代码,该循环仅运行会对分支预测成功产生巨大影响。
但是,是的,您的想法值得考虑。
你可能不想写 CH。这将使前端停滞一个周期,以便在 Haswell/Skylake CPU 上读取 RCX 时自行在一个周期中发出合并 uop。 (Haswell/Skylake上的部分寄存器究竟如何表现?写AL似乎对RAX有错误的依赖,AH不一致)
相反,请考虑设置 CL 和 DL,如果它们都为零,则使用or cl, dl
/jz
跳转。 此外,您可能希望将它们异或归零以避免错误的依赖关系。or
/jz
不能像and
或test
那样将宏融合到单个测试和分支 uop 中,但它仍然比 CH 合并更好(在英特尔 CPU 上)。 您的方式在 Ryzen 上可能会更好,setnz cl
只会合并到现有的 RCX 值中。
在现代英特尔 CPU 上,部分标志合并通常比部分标志合并更有效,因此也许shrx
/test
设置 ZF,然后使用bt ecx, 0
将 setcc 结果放回 CF 而不会干扰 ZF。 (这似乎甚至没有旗帜合并uop:什么是部分旗帜摊位? - BeeOnRope没有报告Skylake上旗帜合并uops的证据。
如果这允许您使用一个依赖于 CF 和 ZF 的分支(如ja
或jbe
)检查这两个条件,那么避免在整数寄存器中实现其中一个布尔值可能会更有效。
如果您需要反转一个或两个布尔值以使其工作:
- 您可以使用
setb
代替setnb
。 - 在针对同一移位掩码进行测试时,您可以使用
andn
而不是test
来反转 RAX。 (错误,我认为这只有在您拥有单位掩码时才有效。
为了避免部分寄存器/虚假依赖恶作剧,您可以考虑使用cmovcc
而不是setcc
;它在Intel Broadwell及更高版本以及AMD上是单UOP。 唯一具有BMI2但2-uop CMOV的主流CPU是Haswell,这不是一场灾难。
IDK,如果有帮助的话;你可能仍然需要将两个寄存器归零,所以你也可以对setcc的目的地这样做,以避免错误的deps。
我认为这确实对某些人有所帮助:我们可以使用test
而不是or
,因此它可以将宏融合到具有jnz
的单个uop中。
xor edx, edx ; can hoist this, or use any other register that's known zero in the low 8.
xor ecx, ecx ; just to avoid false deps. Optional if RCX is cold or part of the input dep chain leading to setnb, on Haswell and later or on AMD.
...
setb cl ; instead of setnb
...
and r11, rax
cmovz ecx, edx ; if ZF is set, make the branch is not taken.
test cl, cl
jz below_and_zero_R11
(我可能翻转了其中一个条件,但您可以在不影响性能的情况下反转 setcc、cmovcc 和 jcc 上的条件,以获得您实际需要的逻辑)。
可能这可以做得更好,并在r11d
本身上cmp
/cmov
非零值,避免setcc
。(将cmp
推迟到生产r11
之后)
shr reg, 53
后,高32位保证为零。 可以使用 32 位操作数大小来保存代码大小(REX 前缀)。 或者,如果您使用的是低 8 个寄存器之一,而不是 r8,则可以使用。建议15. 例如shr rdi, 53
/sub edi, 1023
. 使用r8d
不会节省代码大小,因为由于 r8,它仍然需要 REX 前缀。
将cmp
推迟到最后,以便可以使用adc
而不是setcc
来读取 CF。
setnb
测试 CF=0。 我们可以改用adc
或sbb
来修改setz
或setnz
结果。adc reg,0
是每个支持 BMI2 的 CPU 上的单 uop 指令(只要您避免adc al, imm8
特殊情况编码)。 哪个英特尔微架构引入了 ADC reg,0 单 uop 特例?
(更新:显然adc cl,0
仍然是 2 个 uops 在 Haswell 上。 因此,请改用adc ecx,0
。 由于在此之前对ECX进行了异或归零,因此对于P6系列来说仍然是安全的,不会导致部分寄存器停滞。 如果您依赖于上位为零,则需要在setcc
之前将整个 ECX 归零。
mov r10, 0x1ffffffffffffe ; mask
mov r8, rax
shr r8, 53
sub r8d, 1023
shrx r11, r10, r8
xor ecx, ecx ; avoid false dep
and r11, rax
setnz cl ; r11 == 0
cmp r8, 52 ; r8 < 52 (unsigned)
adc ecx, 0 ; cl = ZF (from r11) + CF (from cmp).
; cl = (r11!=0) + (r8<52)
; test cl, cl ; ADC sets flags
jz @C_2 ; or JNZ, I didn't check the logic
...
@C_2: ; integer
ret
adc ecx,0
只能使 ECX 不为零。 你不能让 CF=1 结果 cl=0 而不依赖于旧cl
。
但是组合条件的另一个选项是sbb ecx, 0
然后检查 CF:仅当 ECX 为零并变为-1
时,才会设置 CF。 即 old_ecx = 0 和 input_CF = 1。
也许只是使用FPU:
如果你有BMI2,你几乎肯定有SSE4.1。 (可能还有AVX)。
如果吞吐量比延迟更重要,请考虑使用roundsd
(或roundpd
一次检查 2
roundpd xmm1, xmm0, something ; TODO: look up what immediate you want for round-to-nearest
pcmpeqq xmm1, xmm0 ; compare the FP bit patterns
movmskpd ecx, xmm1 ; extract the sign bits
; ecx=0b11 if rounding to integer didn't change the bit-pattern
roundpd
/roundsd
是 2 uops。 (https://agner.org/optimize)。
此外,如果您在没有任何其他 FP 操作的情况下连续检查很多内容,那么也许可以考虑只查看 MXCSR 以查看转换是否设置了"不精确"标志。 这涉及通过stmxcsr m32
将MXCSR存储到内存并重新加载,但存储转发使这变得高效。 例如,做一组 8 个,然后检查那个粘性 MXCSR 标志,看看它们中是否有任何是非整数的,然后返回看看它是组中的哪一个。
(如果您真的想要转换结果,那么您可以使用cvtsd2si rax, xmm0
而不是roundsd
)
但是,在操作之前清除不精确标志肯定会增加成本。 但ldmxcsr
并不太贵。 IIRC,现代CPU重命名MXCSR,因此它不会序列化FP操作。