如果有更好的方法来为任意 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/) 上的多个执行端口上运行。唯一可能希望的是它可以将结果放在不同的寄存器中,而不会覆盖输入。
大多数现代非 x86 ISA 都是类似 RISC 的 3 操作数指令,因此移位指令可以复制和移位,不像 x86 移位,编译器还需要 mov到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 位是 not 部分的返回值,并且可以保存任何东西。通常它们为零,因为写入 32 位寄存器隐式地零扩展为 64,但类似return bar() where 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(仅限 Bulldozer-family,不是 Zen)有一个直接形式的 bextr(位域提取),它以 1 uop (https://agner.org/optimize/) 高效运行。 https://godbolt.org/z/bn3rfxzch 显示 gcc11 -O3 -march=bdver4 (Excavator) 使用 bextr rax, rdi, 0x2020,而 clang 错过了该优化。 gcc -march=znver1 使用 mov + shr 因为 Zen 删除了 Trailing Bit Manipulation 以及 XOP 扩展。
Standard BMI1 bextr 需要寄存器中的位置/长度,而在英特尔 CPU 上是 2 微指令,所以它是垃圾。它确实有内在的,但我建议不要使用它。 mov+shr 在 Intel CPU 上更快。