最少的微指令(前端带宽):
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
在大多数具有该功能的 CPU 上,延迟为 3c,吞吐量为 1c(一个端口 1 uop)。
或者 Zen1/2/3 上的 1c,吞吐量为 0.25c。 (https://agner.org/optimize/ https://agner.org/optimize/ & https://uops.info/ https://uops.info/)
在挖掘机之前的推土机系列中,popcnt r64
是 4c 延迟,4c 吞吐量。 (32 位操作数大小具有 2c 吞吐量,但仍有 4c 延迟。)Bobcat 的微编码 popcnt 相当慢。
最低延迟(假设 Haswell 或更新版本,所以有写入 AL 然后读取 EAX 时无部分寄存器效应 https://stackoverflow.com/questions/45660139/how-exactly-do-partial-registers-on-haswell-skylake-perform-writing-al-seems-to,或者没有 P6 血统的 uarch,不会重命名部分寄存器):
2 个周期延迟、2 个微指令、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
AL 是 EAX 的低字节,因此 AL=1 肯定会使 EAX 对于任何非零 RAX 都非零。
在 Sandybridge/Ivy Bridge 上读取 EAX 时,这将花费一个合并微指令。 Core2 / Nehalem 将停滞几个周期以插入合并微指令。早期的 P6 系列(例如 Pentium-M)将完全停止运行,直到setcc
如果后面的指令读取 EAX,则退出。 (为什么 GCC 不使用部分寄存器? https://stackoverflow.com/questions/41573502/why-doesnt-gcc-use-partial-registers)
Nate's neg
/sbb
与 Broadwell 及更高版本上的大致相同,但短 1 个字节。(并将上部 32 归零)。这在 Haswell 上更好,其中sbb
是 2 uop。在早期的主流 Intel CPU 上,它们都需要 3 uop,其中这个在读取 EAX 时需要合并 uop,或者sbb
(以外sbb $0
在 SnB/HSW 上)始终需要 2 uop。 neg/sbb 可以用于不同的寄存器(仍然会破坏输入),但对 AMD 以外的 CPU 具有错误的依赖性。 (K8/K10、Bulldozer 系列和 Zen 系列均认可sbb same,same
仅取决于 CF)。
如果你想把上面的32归零,BMI2RORX https://www.felixcloutier.com/x86/rorx复制并移动:
2 uop、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.
rorx $32
通常对于水平 SWAR 缩减很有用,例如对于双字水平总和,你可以movq
XMM 寄存器中的一对双字,并使用 rorx/add 而不是 pshufd/paddd 在标量中执行最后的 shuffle+add。
或者没有 BMI2,同时仍将上 32 归零:
Intel 上 7 字节、4 微指令、3c 延迟 (where bswap r64
为 2 uops,2c 延迟),否则在高效 CPU 上为 3 uops 2c 延迟bswap r64
比如 Zen 家族和 Silvermont 家族。
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
妥协使用shr $32, %rax
代替bswap
is
8 字节、3 微指令、2c 延迟对于上述情况,即使没有移动消除。
在原始寄存器上运行 ALU 指令,而不是在mov
result 让非消除 MOV 与其并行运行。
评估“最佳”绩效的背景:
-
每条汇编指令需要多少个CPU周期? https://stackoverflow.com/questions/692718/how-many-cpu-cycles-are-needed-for-each-assembly-instruction/44980899#44980899- 这不是绩效的运作方式; CPU 不会等待一条指令完成后再开始下一条指令,并且重叠的可能性取决于细节。
- 预测现代超标量处理器上的操作延迟需要考虑哪些因素以及如何手动计算它们? https://stackoverflow.com/questions/51607391/what-considerations-go-into-predicting-latency-for-operations-on-modern-supersca
-
https://agner.org/optimize/ https://agner.org/optimize/指南和说明表
-
https://uops.info/ https://uops.info/机器生成的指令表(微指令、端口、延迟),没有拼写错误。
-
https://stackoverflow.com/tags/x86/info https://stackoverflow.com/tags/x86/info其他链接
没有成功的想法:
bsf %rax, %rax # 4 bytes. Fails for RAX=1
对于输入 = 0,保留目的地不变。 (AMD 记录了这一点,Intel 实现了它,但没有将其记录为面向未来的。)不幸的是,对于输入 1,这会产生 RAX=0。
并且具有相同的性能popcnt
在 Intel 上,在 AMD 上更糟,但确实节省了 1 个字节的代码大小。
(Using sub $1
设置CF然后??;内特的用法neg
就是如何让它干净利落地工作。)
我没有尝试使用超级优化器 https://en.wikipedia.org/wiki/Superoptimization强力检查其他可能的序列,但正如 Nate 评论的那样,这是一个足够短的问题,可以作为一个用例。