使一个寄存器依赖于另一个而不改变其值

时间:2018-08-02 06:09:23

标签: performance assembly x86 micro-optimization microbenchmark

考虑以下x86程序集:

; something that sets rax
mov rcx, [rdi]
xor rax, rcx
xor rax, rcx

在序列的末尾,rax的值与输入时的值相同,但是从CPU的角度来看,其值取决于从内存加载的值进入rcx。特别是,rax的后续使用要等到该加载和两条xor指令完成后才能开始。

是否有任何方法可以比两个{xor序列更有效地实现此效果,例如,使用单个单向单周期等待时间指令?如果需要在序列之前设置一次恒定值(例如,将寄存器清零),就可以了。

1 个答案:

答案 0 :(得分:4)

目标寄存器的关键路径上只有1 uop / 1c的延迟:

# target=rax  extra source=rcx
mov  edx, ecx    ; no latency
and  edx, 0      ; BMI1  ANDN could mov+and in 1 uop, port 1 or 5 only on SnB-family (Ryzen: any)

or   rax, rdx

在任何CPU AFAIK上,AND为零的情况都不是a dep-breaking zeroing idiom的特殊情况。

前端微指令:3(或BMI1为2)。延迟时间:

  • 从rcx到rax:2c(具有消除功能或BMI1)。
  • 从rax(输入)到rax(输出):1c

如果寄存器为零,则可以将所有dep链耦合到该寄存器中(不同于仅读取全是寄存器的ANDN版本):

and   edx, ecx         # 0 &= ecx
or    rax, rdx         # rax |= 0

要测试功能的延迟(不是吞吐量),但仍要重复输入相同的输入

.loop:
    call  func        ; arg in RDI, return in RAX
    mov   rdi, rbx    ; arg for next iter, off the critical path

    and   eax, 0      ; 1c latency
    or    rdi, rax    ; 1c latency

   jmp   .loop

如果函数是纯函数,我们可以做1c / 1uop

实际上,它只需要为给定的输入返回一个已知值。如果其杂质被限制为具有其他副作用/输出,这也将起作用。

设置结果,而不是两次获取XOR,然后进行设置,以使我们已经有了XOR,我们仅需再进行一次XOR即可进行加扰。或使用加法,因为LEA允许我们在一条指令中进行复制和加法,从而保存了不在关键路径上的mov

    mov   rdi, rbx        ; original input
    call  func
    sub   rbx, rax        ; RBX = input - output

.loop:
    call  func
    lea   rdi, [rbx + rax]   ; RDI = (input-output) + output = input
    jmp  .loop

@RossRidge的建议在SnB系列CPU上只有1个uop,但只能在端口1上运行。

shld rax, rcx, 0

3c延迟,HSW / SKL上的端口1为1 uop。 Agner Fog报告IvB延迟为1c,HSW / BDW / SKL延迟为3c。

shld r,r,i在较老的Intel上为2 oups,在AMD上显着降低,例如在PILEDRIVER / Ryzen上为6 oups / 3c延迟。

请注意,instlatx64报告了Haswell / Skylake上的shld / shrd的1c延迟/ 0.5c吞吐量(如单寄存器移位),但我进行了自我测试,绝对是3c延迟/ 1c吞吐量。 Reported as an instlatx64 bug on their github page

SHLD对于复制依赖于另一个寄存器的32位寄存器也可能很有趣。例如@BeeOnRope描述了想要重复调用RDI中具有相同输入值但依赖于RAX中结果的函数。如果我们只关心EDI,那么

; RBX = input<<32
call  func
mov   edi, eax         ; 0 latency with mov-elimination
shld  rdi, rbx, 32     ; EDI = the high 32 bits of RBX, high bits of RDI = old EDI.

相对于此,这当然是没有意义的,不需要运动消除

call   func
mov    rdi, rbx        ; off critical path
shld   rdi, rax, 0     ; possibly 1c latency on SnB / IvB.  3 on HSW/SKL

对@DavidWholford的建议的修改也有效

test ecx,ecx     ; CF=0, with a false dependency on RCX
adc  rax, 0      ; dependent on CF
在Haswell / Broadwell / Skylake和AMD上

2。英特尔P6系列产品可能只有3微秒,甚至可能是SnB / IvB。延迟时间:

  • 从rcx到rax:在HSW和更高版本上为2c,在2 uop adc下为3
  • 从rax到rax:在HSW和更高版本上为1c,在2 uop adc下为2
在Haswell及更早版本上的

ADC通常为2 oups,但在Haswell上特殊设置的立即数为 adc仅为1 uop / 1c adc eax,0在Core 2上始终是2c延迟。进行此优化的第一个方法可能是SnB,但希望我们在Which Intel microarchitecture introduced the ADC reg,0 single-uop special case?上得到答案

test清除CF而不管其值如何,但我认为(未测试)CF仍然依赖于源寄存器。如果没有,那么在Broadwell及更高版本上使用TEST / ADOX可能会有用。 (因为CF在大多数CPU上是单独重命名的,但是OF可能仅与ZF / SF属于同一捆绑包,并且其他标志确实取决于AND结果。)