有效地检查 FP 位模式是否为整数.在条件组合上分支一次的速度更快



我有下一个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不能像andtest那样将宏融合到单个测试和分支 uop 中,但它仍然比 CH 合并更好(在英特尔 CPU 上)。 您的方式在 Ryzen 上可能会更好,setnz cl只会合并到现有的 RCX 值中。


在现代英特尔 CPU 上,部分标志合并通常比部分标志合并更有效,因此也许shrx/test设置 ZF,然后使用bt ecx, 0将 setcc 结果放回 CF 而不会干扰 ZF。 (这似乎甚至没有旗帜合并uop:什么是部分旗帜摊位? - BeeOnRope没有报告Skylake上旗帜合并uops的证据。

如果这允许您使用一个依赖于 CF 和 ZF 的分支(如jajbe)检查这两个条件,那么避免在整数寄存器中实现其中一个布尔值可能会更有效。

如果您需要反转一个或两个布尔值以使其工作:

  • 您可以使用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。 我们可以改用adcsbb来修改setzsetnz结果。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操作。

相关内容

最新更新