问题描述
想象以下代码:
uint64_t x = 0x81C6E3292A71F955ULL;
uint32_t y = (uint32_t) (x >> 32);
y
接收 64 位整数的较高 32 位部分。我的问题是是否有任何内在函数或任何 cpu 指令可以在不进行移动和移位的情况下在单个操作中执行此操作?
至少 CLang(在上面的 Try-it-online 中链接)为此创建了两条指令 mov rax,rdi
和 shr rax,32
,所以要么 CLang 不做这样的优化,要么不存在这样的特殊指令.
如果存在像 movhi dst_reg,src_reg
这样的假想单指令就好了。
解决方法
如果有更好的方法来对任意 uint64_t 进行位域提取,编译器就会使用它。 (至少在理论上;编译器确实错过了优化,他们的选择有时会偏向延迟,即使它会花费更多的 uops。)
对于无法用纯 C 语言以编译器已经很容易理解的方式有效表达的内容,您只需要内在函数。(或者如果您的编译器很笨,无法发现明显的.)
您可以想象输入值来自两个 32 位值相乘的情况,那么在某些 CPU 上编译器使用加宽 mul r32
已经生成两个独立的结果可能是值得的32 位寄存器,而不是 imul r64,r64
+ shr reg,32
,如果它可以轻松使用 EAX/EDX。但除了 gcc -mtune=silvermont
或其他调整选项,您不能让编译器那样做。
shr reg,32
具有 1 个周期延迟,并且可以在大多数现代 x86 微体系结构 (https://uops.info/) 上的 1 个以上执行端口上运行。唯一可能希望的是它可以将结果放在不同的寄存器中,而不会覆盖输入。
大多数现代非 x86 ISA 都类似于具有 3 操作数指令的 RISC,因此移位指令可以复制和移位,这与编译器需要 mov
的 x86 移位不同除了 shr
如果以后还需要原始 64 位值,或者(对于小函数)需要不同寄存器中的返回值。
有些 ISA 具有位域提取指令。 PowerPC 甚至有一个有趣的旋转和掩码指令 (rlwinm
)(掩码是由立即数指定的位范围),它与普通移位指令不同。编译器将根据需要使用它 - 不需要内在函数。 https://devblogs.microsoft.com/oldnewthing/20180810-00/?p=99465
x86 使用 BMI2 has rorx rax,rdi,32
进行复制和旋转,而不是在同一寄存器内卡住移位。在不内联的独立版本中,返回 uint32_t
的函数可以/应该使用它而不是 mov+shr,因为调用者已经不得不忽略 RAX 中的高垃圾。 (x86-64 System V 和 Windows x64 都将返回值定义为仅与 arg 的 C 类型匹配的寄存器宽度;例如,返回 uint32_t
表示 RAX 的高 32 位 不是 返回值的一部分,并且可以保存任何东西。通常它们为零,因为写入 32 位寄存器隐式地将零扩展到 64,但是像 return bar()
之类的东西 bar 返回 uint64_t 可以让 RAX 保持不变而无需截断它;实际上优化的尾调用是可能的。)
rorx
没有内在函数;编译器应该知道什么时候使用它。 (但是 gcc/clang -O3 -march=haswell
错过了这个优化。)https://godbolt.org/z/ozjhcc8Te
如果编译器在循环中执行此操作,则它可以将 32
放在 shrx reg,reg,reg
的寄存器中作为复制和移位。或者更愚蠢的是,它可以使用 pext
和 0xffffffffULL << 32
作为掩码。但这比 shrx
更糟糕,因为延迟更高。
AMD TBM(仅限推土机系列,非 Zen)具有直接形式的 bextr
(位域提取),并且它以 1 uop (https://agner.org/optimize/) 高效运行。 https://godbolt.org/z/bn3rfxzch 显示 gcc11 -O3 -march=bdver4
(挖掘机)使用 bextr rax,0x2020
,而 clang 错过了该优化。 gcc -march=znver1
使用 mov + shr,因为 Zen 删除了尾随位操作以及 XOP 扩展。
Standard BMI1 bextr
需要寄存器中的位置/长度,在 Intel CPU 上是 2 uop,因此它是垃圾。它确实有一个内在的,但我建议不要使用它。 mov
+shr
在 Intel CPU 上速度更快。