Instruction/intrinsic 在 C++ 中占了 uint64_t 的一半?

Instruction/intrinsic for taking higher half of uint64_t in C++?

想象以下代码:

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 执行此位域提取,编译器早就会使用它了。 (至少在理论上;编译器确实错过了优化,他们的选择有时有利于延迟,即使它花费更多的微指令。)

对于无法在纯 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 都是类似 RISC 的 3 操作数指令,所以移位指令可以复制和移位,不像 x86 移位,编译器需要一个mov 除了 shr 如果以后还需要原始 64 位值,或者(在微型函数的情况下)需要不同寄存器中的 return 值。

并且一些 ISA 具有位域提取指令。 PowerPC 甚至有一个有趣的旋转和掩码指令 (rlwinm)(掩码是由立即数指定的位范围),它是与正常移位不同的指令。编译器将酌情使用它——不需要内在函数。 https://devblogs.microsoft.com/oldnewthing/20180810-00/?p=99465


x86 与 BMI2 has rorx rax, rdi, 32 复制和旋转,而不是卡在同一个寄存器内移位。函数 returning uint32_t could/should 使用它而不是 mov+shr,在不内联的独立版本中,因为调用者已经必须忽略 RAX 中的高垃圾。 (x86-64 系统 V 和 Windows x64 都将 return 值定义为仅与 arg 的 C 类型匹配的寄存器宽度;例如 returning uint32_t 意味着RAX 的高 32 位 不是 return 值的一部分,并且可以容纳任何东西。通常它们是零,因为写入 32 位寄存器隐式零扩展到 64 ,但是像 return bar() 这样的东西,其中 bar returns uint64_t 可以保持 RAX 不变,而不必 t运行cate 它;事实上,优化的尾调用是可能的。)

rorx 没有内在函数;编译器只是应该知道何时使用它。 (但是 gcc/clang -O3 -march=haswell 错过了这个优化。) https://godbolt.org/z/ozjhcc8Te

如果编译器在循环中执行此操作,它可以在 shrx reg,reg,reg 的寄存器中包含 32 作为复制和移位。或者更愚蠢的是,它可以使用 pext0xffffffffULL << 32 作为掩码。但这比 shrx 更糟,因为延迟更高。

AMD TBM (Bulldozer-family only, not Zen) had an immediate form of bextr (bitfield-extract), and it ran efficiently as 1 uop (https://agner.org/optimize/). https://godbolt.org/z/bn3rfxzch 显示 gcc11 -O3 -march=bdver4(挖掘机)使用 bextr rax, rdi, 0x2020,而 clang 错过了该优化。 gcc -march=znver1 使用 mov + shr 因为 Zen 放弃了尾随位操作和 XOP 扩展。

Standard BMI1 bextr 在寄存器中需要 position/len,而在 Intel CPU 上是 2 微指令,所以它是垃圾。它确实有一个内在的,但我建议不要使用它。 mov+shr 在 Intel CPU 上更快。