x86_64 将 64 位寄存器减少到 32 位并保留零或非零状态的最佳方法

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 个。

  • @CodyGray:确实,如果您指定“-1”作为非零结果返回的值,这就是 gcc 和 clang 所做的:https://godbolt.org/z/oPMdYn7v1 (2认同)

归档时间:

查看次数:

3239 次

最近记录:

3 年,2 月 前