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

Bee*_*ope 3 performance x86 assembly micro-optimization microbenchmark

考虑以下 x86 程序集:

; something that sets rax
mov rcx, [rdi]
xor rax, rcx
xor rax, rcx
Run Code Online (Sandbox Code Playgroud)

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

有没有什么方法可以比双xor序列更有效地实现这种效果,例如,使用单个单微指令单周期延迟指令?如果某个常量值需要在序列之前设置一次(例如,有一个归零的寄存器),这是可以的。

Pet*_*des 5

目标寄存器的关键路径上只有 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
Run Code Online (Sandbox Code Playgroud)

AFAIK ,AND with 0 并不是任何 CPU 上的特殊置零惯用语

前端微指令:3(或 BMI1 时为 2)。潜伏:

  • 从 rcx 到 rax:2c(使用 mov-elimination 或 BMI1)。
  • 从 rax(输入)到 rax(输出):1c

使用归零的寄存器,如果可以将所有 dep 链耦合到该一个寄存器中(与仅读取全 1 寄存器的 ANDN 版本不同):

and   edx, ecx         # 0 &= ecx
or    rax, rdx         # rax |= 0
Run Code Online (Sandbox Code Playgroud)

要测试函数的延迟(不是吞吐量),但仍重复向其提供相同的输入

.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
Run Code Online (Sandbox Code Playgroud)

如果函数是纯函数,我们可以做 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
Run Code Online (Sandbox Code Playgroud)

@RossRidge 的建议是 SnB 系列 CPU 上只有 1 uop,但仅在端口 1 上运行:

shld rax, rcx, 0
Run Code Online (Sandbox Code Playgroud)

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

shld r,r,i在较旧的 Intel 上为 2 uops,在 AMD 上明显慢一些,例如 Piledriver / Ryzen 上的 6 uops / 3c 延迟。

请注意,instlatx64报告 Haswell/Skylake 上 shld/shrd 的 1c 延迟/0.5c 吞吐量(如单寄存器移位),但我测试了自己,它绝对是 3c 延迟/1c 吞吐量。 在其 github 页面上报告为 instlatx64 bug

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.
Run Code Online (Sandbox Code Playgroud)

当然,这与不需要移动消除的 this 相比毫无意义

call   func
mov    rdi, rbx        ; off critical path
shld   rdi, rax, 0     ; possibly 1c latency on SnB / IvB.  3 on HSW/SKL
Run Code Online (Sandbox Code Playgroud)

对 @DavidWholford 建议的修改也有效

test ecx,ecx     ; CF=0, with a false dependency on RCX
adc  rax, 0      ; dependent on CF
Run Code Online (Sandbox Code Playgroud)

Haswell/Broadwell/Skylake 和 AMD 上为 2 uops。Intel P6 系列上为 3 uop,也许还有 SnB/IvB。潜伏:

  • 从 rcx 到 rax:HSW 及更高版本上为 2c,使用 2-uop adc 为 3
  • 从 rax 到 rax:HSW 上的 1c 及更高版本,2 个带 2-uop adc 的

Haswell 及更早版本上的 ADC 通常为 2 uop,但adc在 Haswell 上,立即数 0 是特殊情况,仅为 1 uop / 1cadc eax,0Core 2 上的延迟始终为 2c。第一个进行此优化的 uarch 可能是 SnB,但希望我们能得到关于“哪种 Intel 微架构引入了 ADC reg,0 单 uop 特殊情况?”的答案。

test无论值如何,都会清除 CF,但我认为(未经测试)CF 仍然依赖于源寄存器。如果没有,那么也许使用 TEST / ADOX 在 Broadwell 及更高版本上可能有用。(因为 CF 在大多数 CPU 上被单独重命名,但 OF 可能只是与 ZF / SF 和其他依赖于 AND 结果的标志属于同一包的一部分。)

  • @PeterCordes - 谣言是真的!在 Haswell 上,“adc rax, 0”有 1 个周期延迟,“adc rax, 1”有 2 个周期延迟。配方吞吐量为 0.5 vs 0.76(请记住,其中也有 2 个异或,因此,如果“adc”为 2 uops,则 0.75 是最大可能的 rtput)。 (2认同)