【问题标题】:Instruction/intrinsic for taking higher half of uint64_t in C++?在 C++ 中获取 uint64_t 的上半部分的指令/内在函数?
【发布时间】:2021-08-08 07:23:35
【问题描述】:

想象下面的代码:

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 这样的虚构单条指令会很棒。

【问题讨论】:

  • 为什么不换班?为什么你认为一些内在会比转变更好? Shift 正是你想要的,那为什么还要有任何特殊说明呢?
  • 如果有一些秘密指令可以通过常数 32 优化右移,我希望编译器知道并应用它。
  • " 是否有任何 CPU 指令在不执行移位的情况下执行此操作?" --> C没有指定CPU指令
  • @Arty 使用这样的联合可能根本不涉及内存,因为编译器已经是 storing small structs in registers。这可能是过早的优化,我认为任何架构都没有这样的指令。一些 16 位机器有交换指令来交换 2 个字节,在某些情况下可以用来实现这一点

标签: c++ c bit-manipulation intrinsics instructions


【解决方案1】:

如果有更好的方法来为任意 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 移位,编译器还需要 movshr,如果它以后还需要原始的 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 的寄存器中作为复制和移位。或者更愚蠢的是,它可以使用pext0xffffffffULL << 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 上更快。

【讨论】:

  • 您能告诉我们是否有很多(尤其是旧的)CPU 支持这个 BMI2 集?全世界大约有百分之几的 CPU 具有 BMI2?
  • 对于我来说,rorx 不存在内在函数也很奇怪。我认为所有虚构指令都存在内在函数,只是为了方便,如果我不想依赖编译器猜测而是明确使用特定指令。
  • @Arty:BMI2 在 Haswell 中是 Intel 的新功能,AMD 也有类似的一代。 en.wikipedia.org/wiki/Bit_manipulation_instruction_set。云服务器会有它,但有很多家用 CPU 没有。一些编译器通常具有用于旋转的内在函数 (Best practices for circular shift (rotate) operations in C++),但 RORX 和 ROR 之间的唯一区别是没有设置 FLAGS,并且具有单独的目标。寄存器分配和管理 FLAGS 完全取决于编译器,因此强制它使用 RORX 而不是 ROR 是没有意义的。
  • “大多数现代非 x86 ISA 都与 RISC 类似,带有 3 操作数指令”--> 是否考虑到如今大多数处理器是每年数十亿的嵌入式处理器?
  • @chux-ReinstateMonica:不是真的。我想到了可用于高性能计算的 ISA,例如 AArch64 和 PowerPC64。如果您像大多数嵌入式使用一样拥有 32 位 ISA,则 uint64_t 的两半已经是分开的。或者对于 ARM 的 uint32_t 的 16 位一半,即使在拇指模式下,我认为 lsr dst, src, 16 也可以编码为单个拇指指令。即使是没有桶形移位器的低端嵌入式东西也是一种不同的野兽,但通常有某种交换指令而不需要移位,编译器可以使用它来实现x >> 8x >> 16
猜你喜欢
  • 2020-12-03
  • 2020-09-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多