【问题标题】:Make a register depend on another one without changing its value使一个寄存器依赖另一个寄存器而不改变它的值
【发布时间】:2019-01-09 19:27:38
【问题描述】:

考虑以下 x86 程序集:

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

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

有没有什么方法比两个-xor 序列更有效地实现这种效果,例如,使用单个 1 uop 1 周期延迟指令?如果某个常数值需要在序列之前设置一次(例如,有一个归零的寄存器),那也没关系。

【问题讨论】:

  • 可能是shld rax, rcx, 0?还是imul rax, rcx, 1
  • @RossRidge,啊,这些都不错。太糟糕了,它们是三个周期的延迟(在我的特定情况下,这恰好比 uop 计数更重要)。我本可以在问题中说得更清楚......
  • @phuclv 2 次将rax 恢复为原始值,即创建“nop”操作,但依赖于从内存加载的rcx 值。 BeeOnRope 正在研究某种微基准,执行现代 x86 架构的各种内部约束,所以他的代码有时看起来有点奇怪,这只是一段人工的基准代码,而不是做一些“有意义”的“真实”代码。
  • @RossRidge imul rax, rcx, 1 会将rcx 移动到rax,从而破坏rax 的内容? ...如果rcx 可能被破坏,也许imul rcx, rax 会按需要工作。 = 不,这使得rax 独立,UPS。对我来说太早了......
  • 我的思路是xor ecx, ecx ; add rcx, [edi] ; adc rax, 0

标签: performance assembly x86 micro-optimization microbenchmark


【解决方案1】:

目标寄存器的关键路径上只有 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 上都不是特殊情况下的 a dep-breaking zeroing idiom,AFAIK。

前端微指令:3 个(或 2 个 BMI1)。延迟:

  • 从 rcx 到 rax:2c(使用 mov-elimination 或 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 uops,在 AMD 上显着降低,例如 Piledriver / Ryzen 上的 6 uops / 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.

当然,这与不需要 mov-elimination 的 this 相比毫无意义

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:1c 在 HSW 及更高版本上,2 使用 2-uop adc

Haswell 和更早版本的 ADC 通常为 2 uop,但 adc 立即为 0 在 Haswell 上是特殊情况,仅为 1 uop / 1cadc eax,0 在 Core 2 上总是 2c 延迟。第一个具有这种优化的 uarch 可能是 SnB,但希望我们能在 Which Intel microarchitecture introduced the ADC reg,0 single-uop special case? 上得到答案

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

【讨论】:

  • 你介意帮我理解第一部分吗?特别是“从 rax 到 rax”的延迟?谢谢!
  • @MargaretBloom:RAX(input) 到 RAX(output),即在 or rax,rdx 之前/之后,因为这是唯一涉及 RAX 的指令。如果关键路径是通过 RAX 的 dep 链,则设置 RDX 的指令不在关键路径上。
  • @PeterCordes - 谣言是真的! adc rax, 0 有 1 个周期延迟,adc rax, 1 在 Haswell 上有 2 个周期延迟。接收吞吐量为 0.5 与 0.76(请记住,其中也有 2 个异或,因此如果 adc 为 2 微秒,则 0.75 是最大可能的 rtput)。
  • 我想我可能有点错误地指定了这个问题,因为 1 周期延迟的想法(mov;and;or 之一)在大多数实际场景中都不起作用。我认为实际情况类似于一个 1-arg 函数(rdi 中的 arg),它返回 1 个结果(rax 中)。所以我需要让rdi 下一次调用取决于rax。在实践中,rdi 也需要重置其值,因为它被调用者破坏了,比如说来自rbx。所以我想我在实践中得到了 2 个周期的“rcx to rax”成本。我没有看到让它 1 个周期的方法。
  • @BeeOnRope:对于纯函数,将 input - output 保存在寄存器中,并使用 LEA 重新生成 input。再次更新了我的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-02
  • 2016-10-28
  • 1970-01-01
  • 2019-12-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多