使一个寄存器依赖于另一个寄存器而不改变它的值
Make a register depend on another one without changing its value
考虑以下 x86 程序集:
; something that sets rax
mov rcx, [rdi]
xor rax, rcx
xor rax, rcx
在序列的末尾,rax
与输入时的值相同,但从 CPU 的角度来看,其值 取决于 从内存加载到 rcx
的值。特别是,在该加载和两个 xor
指令完成之前,rax
的后续使用不会开始。
有没有比双xor
序列更有效地实现这种效果的方法,例如,使用单个单微指令单周期延迟指令?如果需要在序列之前设置一次某个常量值(例如,有一个归零寄存器),这没关系。
目标寄存器的关键路径上只有 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
AND 为零在任何 CPU 上都不是特殊情况 ,AFAIK。
前端微指令:3(或 BMI1 为 2)。延迟:
- 从 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
实际上它只需要 return 给定输入的已知值。如果其杂质仅限于产生其他副作用/输出,这也有效。
不是在得到结果后进行两次 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 微指令,在 AMD 上慢得多,比如在 Piledriver / Ryzen 上是 6 微指令/3c 延迟。
注意 instlatx64 reports 1c latency / 0.5c throughput for shld/shrd on Haswell/Skylake (like single-register shifts), but I tested myself and it's definitely 3c latency / 1c throughput. 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
2 微指令在 Haswell/Broadwell/Skylake 和 AMD 上。 Intel P6 系列上的 3 微指令,也许 SnB/IvB。延迟:
- 从 rcx 到 rax:HSW 上的 2c 及更高版本,3 与 2-uop adc
- 从 rax 到 rax:HSW 上的 1c 及更高版本,2 使用 2-uop adc
Haswell 和更早版本的 ADC 通常为 2 微指令,但立即数为 0 的 adc
在 Haswell 上是特殊情况,仅为 1 微指令/1c。 adc eax,0
在 Core 2 上始终是 2c 延迟。第一个进行此优化的 uarch 可能是 SnB,但希望我们在
上得到答案
test
清除 CF 而不管其值如何,但我 认为 (未测试)CF 仍然依赖于源寄存器。如果没有,那么使用 TEST / ADOX 可能对 Broadwell 及更高版本有用。 (因为 CF 在大多数 CPU 上被单独重命名,但 OF 可能只是与 ZF / SF 和其他依赖于 AND 结果的标志相同的包的一部分。)
考虑以下 x86 程序集:
; something that sets rax
mov rcx, [rdi]
xor rax, rcx
xor rax, rcx
在序列的末尾,rax
与输入时的值相同,但从 CPU 的角度来看,其值 取决于 从内存加载到 rcx
的值。特别是,在该加载和两个 xor
指令完成之前,rax
的后续使用不会开始。
有没有比双xor
序列更有效地实现这种效果的方法,例如,使用单个单微指令单周期延迟指令?如果需要在序列之前设置一次某个常量值(例如,有一个归零寄存器),这没关系。
目标寄存器的关键路径上只有 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
AND 为零在任何 CPU 上都不是特殊情况
前端微指令:3(或 BMI1 为 2)。延迟:
- 从 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
实际上它只需要 return 给定输入的已知值。如果其杂质仅限于产生其他副作用/输出,这也有效。
不是在得到结果后进行两次 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.
的延迟为 3cshld r,r,i
在旧的 Intel 上是 2 微指令,在 AMD 上慢得多,比如在 Piledriver / Ryzen 上是 6 微指令/3c 延迟。
注意 instlatx64 reports 1c latency / 0.5c throughput for shld/shrd on Haswell/Skylake (like single-register shifts), but I tested myself and it's definitely 3c latency / 1c throughput. 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
2 微指令在 Haswell/Broadwell/Skylake 和 AMD 上。 Intel P6 系列上的 3 微指令,也许 SnB/IvB。延迟:
- 从 rcx 到 rax:HSW 上的 2c 及更高版本,3 与 2-uop adc
- 从 rax 到 rax:HSW 上的 1c 及更高版本,2 使用 2-uop adc
Haswell 和更早版本的 ADC 通常为 2 微指令,但立即数为 0 的 adc
在 Haswell 上是特殊情况,仅为 1 微指令/1c。 adc eax,0
在 Core 2 上始终是 2c 延迟。第一个进行此优化的 uarch 可能是 SnB,但希望我们在
test
清除 CF 而不管其值如何,但我 认为 (未测试)CF 仍然依赖于源寄存器。如果没有,那么使用 TEST / ADOX 可能对 Broadwell 及更高版本有用。 (因为 CF 在大多数 CPU 上被单独重命名,但 OF 可能只是与 ZF / SF 和其他依赖于 AND 结果的标志相同的包的一部分。)