Noa*_*oah 38 assembly x86-64 micro-optimization
我正在寻找最快/最节省空间的方法,将 64 位寄存器减少为 32 位寄存器,仅保留 64 位寄存器的零/非零状态。
我目前适用于所有值的最佳想法是popcntq
(1c tput,主流英特尔上的 3c 延迟,5 字节代码大小):
// rax is either zero or non-zero
popcntq %rax, %rax
// eax will be zero if rax was zero, otherwise it will be non-zero
Run Code Online (Sandbox Code Playgroud)
注意:直接使用 32 位是行不通的eax:如果rax说 的2^61零/非零状态eax与 的不同rax
有没有更好的巧妙方法?
Pet*_*des 31
最少 uops(前端带宽):
1 uop,延迟 3c (Intel) 或 1c (Zen)。
也是最小的代码大小,5 字节。
popcnt %rax, %rax # 5 bytes, 1 uop for one port
# if using a different dst, note the output dependency on Intel before ICL
Run Code Online (Sandbox Code Playgroud)
在大多数具有该功能的 CPU 上,延迟为 3c,吞吐量为 1c(一个端口 1 uop)。
或者 Zen1/2/3 上的 1c,吞吐量为 0.25c。(https://agner.org/optimize/和https://uops.info/)
在 Excavator 之前的 Bulldozer 系列中,popcnt r64延迟为 4c,吞吐量为 4c。(32 位操作数大小具有 2c 吞吐量,但仍有 4c 延迟。)Bobcat 的微编码 popcnt 相当慢。
最低延迟(假设 Haswell 或更新版本,因此在写入 AL 然后读取 EAX 时不会出现部分寄存器效应,或者没有 P6 血统的 uarch 不会重命名部分寄存器):
2 个周期延迟、2 uops、6 字节。 如果 popcnt (5B) 不可用,也是最小的代码大小。
test %rax, %rax # 3B, 1 uop any ALU port
setnz %al # 3B, 1 uop p06 (Haswell .. Ice Lake)
# only works in-place, with the rest of EAX already being known 0 for RAX==0
Run Code Online (Sandbox Code Playgroud)
AL 是 EAX 的低字节,因此 AL=1 肯定会使 EAX 对于任何非零 RAX 都非零。
在 Sandybridge/Ivy Bridge 上读取 EAX 时,这将花费一个合并微指令。Core2 / Nehalem 将停滞几个周期以插入合并微指令。setcc如果后面的指令读取 EAX,早期的 P6 系列(例如 Pentium-M)将完全停止,直到退出。(为什么GCC不使用部分寄存器?)
Nate 的neg/sbb与 Broadwell 及更高版本上的大致相同,但短了 1 个字节。 (并将上部 32 归零)。这在 Haswell 上更好,那里sbb有 2 uops。在早期的主流 Intel CPU 上,它们都需要 3 uops,其中这个在读取 EAX 时需要合并 uop,或者sbb(SnB/HSW 除外sbb $0)始终需要 2 uops。neg/sbb 可以用于不同的寄存器(仍然会破坏输入),但对 AMD 以外的 CPU 具有错误的依赖性。(K8/K10、Bulldozer 系列和 Zen 系列都认为sbb same,same仅依赖于 CF)。
如果您希望高 32 位归零,BMI2 RORX进行复制和移位:
2 uops,2c 延迟,8 字节
rorx $32, %rax, %rdx # 6 bytes, 1 uop, 1c latency
or %edx, %eax # 2 bytes, 1c latency
## can produce its result in a different register without a false dependency.
Run Code Online (Sandbox Code Playgroud)
rorx $32通常对于水平 SWAR 缩减很有用,例如,对于双字水平总和,您可以movq从 XMM 寄存器中取出一对双字,并使用 rorx/add 而不是 pshufd/paddd 在标量中执行最后的 shuffle+add。
或者没有 BMI2,同时仍将上 32 位归零:
Intel 上的 7 字节、4 uops、3c 延迟(其中bswap r642 uops、2c 延迟),否则在bswap r64Zen 系列和 Silvermont 系列等高效 CPU 上为 3 uops 2c 延迟。
mov %eax, %edx # 2 bytes, and not on the critical path
bswap %rax # 3 bytes, vs. 4 for shr $32, %rax
or %edx, %eax # 2 bytes
## can write a different destination
Run Code Online (Sandbox Code Playgroud)
即使没有 mov 消除,也可以使用8字节、3 uops、2c 延迟shr $32, %rax来代替上述内容。
在原始寄存器而不是结果上运行 ALU 指令可以让非消除 MOV 与其并行运行。bswap
mov
评估“最佳”绩效的背景:
没有成功的想法:
bsf %rax, %rax # 4 bytes. Fails for RAX=1
Run Code Online (Sandbox Code Playgroud)
对于输入 = 0,保留目的地不变。(AMD 记录了这一点,Intel 实现了它,但没有将其记录为面向未来的。)不幸的是,对于输入 1,这会产生 RAX=0。
并且具有与 Intel 相同的性能popcnt,在 AMD 上更差,但确实节省了 1代码大小的字节。
(使用sub $1设置 CF,然后 ??;Nate 的用法neg是如何让它干净利落地工作。)
我没有尝试使用超级优化器来强力检查其他可能的序列,但正如 Nate 评论的那样,这是一个足够短的问题,可以作为一个用例。
Nat*_*dge 24
一种选择是
neg rax ; 48 F7 D8
sbb eax, eax ; 19 C0
Run Code Online (Sandbox Code Playgroud)
请记住,neg设置标志就像从零减去一样,因此它设置进位标志当且rax仅当非零。sbb寄存器本身产生或根据进位是否清除或设置(感谢@prl在评论中建议这一点)0。-1
它仍然是 5 个字节,2 个 uops,而不是 1 个。但如果我的数学是正确的,在 Skylake 上,你会得到 2 个周期延迟,而不是 3 个周期,每个周期的吞吐量为 2 个周期,而不是 1 个。
| 归档时间: |
|
| 查看次数: |
3239 次 |
| 最近记录: |