在 C++ 中使用 uint64_t 的上半部分的指令/内在?

问题描述

想象以下代码

Try it online!

uint64_t x = 0x81C6E3292A71F955ULL;
uint32_t y = (uint32_t) (x >> 32);

y 接收 64 位整数的较高 32 位部分。我的问题是是否有任何内在函数或任何 cpu 指令可以在不进行移动和移位的情况下在单个操作中执行此操作?

至少 CLang(在上面的 Try-it-online 中链接)为此创建了两条指令 mov rax,rdishr 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 的寄存器中作为复制和移位。或者更愚蠢的是,它可以使用 pext0xffffffffULL << 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 上速度更快。