TL:DR with GCC for a 64-bit ISA: (a * (unsigned __int128)b) >> 64 可以很好地编译为单个全乘法或高半乘法指令。无需使用内联汇编。
不幸的是,当前的编译器不优化 @craigster0 的可移植版本,所以如果你想利用 64 位 CPU,你不能使用它,除非作为您没有#ifdef 的目标的后备。 (我没有看到优化它的通用方法;您需要 128 位类型或内在类型。)
GNU C(gcc、clang 或 ICC)has unsigned __int128 在大多数 64 位平台上。 (或者在旧版本中,__uint128_t)。不过,GCC 并没有在 32 位平台上实现这种类型。
这是让编译器发出 64 位全乘法指令并保留高半部分的简单而有效的方法。 (GCC 知道 uint64_t 转换为 128 位整数的上半部分仍然全为零,因此您不会使用三个 64 位乘法得到 128 位乘法。)
MSVC also has a __umulh intrinsic 用于 64 位高半乘法,但同样它仅适用于 64 位平台(特别是 x86-64 和 AArch64。文档还提到了具有 _umul128 可用的 IPF (IA-64),但我没有适用于 Itanium 的 MSVC。(反正可能不相关。)
#define HAVE_FAST_mul64 1
#ifdef __SIZEOF_INT128__ // GNU C
static inline
uint64_t mulhi64(uint64_t a, uint64_t b) {
unsigned __int128 prod = a * (unsigned __int128)b;
return prod >> 64;
}
#elif defined(_M_X64) || defined(_M_ARM64) // MSVC
// MSVC for x86-64 or AArch64
// possibly also || defined(_M_IA64) || defined(_WIN64)
// but the docs only guarantee x86-64! Don't use *just* _WIN64; it doesn't include AArch64 Android / Linux
// https://docs.microsoft.com/en-gb/cpp/intrinsics/umulh
#include <intrin.h>
#define mulhi64 __umulh
#elif defined(_M_IA64) // || defined(_M_ARM) // MSVC again
// https://docs.microsoft.com/en-gb/cpp/intrinsics/umul128
// incorrectly say that _umul128 is available for ARM
// which would be weird because there's no single insn on AArch32
#include <intrin.h>
static inline
uint64_t mulhi64(uint64_t a, uint64_t b) {
unsigned __int64 HighProduct;
(void)_umul128(a, b, &HighProduct);
return HighProduct;
}
#else
# undef HAVE_FAST_mul64
uint64_t mulhi64(uint64_t a, uint64_t b); // non-inline prototype
// or you might want to define @craigster0's version here so it can inline.
#endif
对于 x86-64、AArch64 和 PowerPC64(和其他),这编译为一条 mul 指令,以及一对 movs 来处理调用约定(应该优化在此内联之后离开)。
来自the Godbolt compiler explorer(带有 x86-64、PowerPC64 和 AArch64 的 source + asm):
# x86-64 gcc7.3. clang and ICC are the same. (x86-64 System V calling convention)
# MSVC makes basically the same function, but with different regs for x64 __fastcall
mov rax, rsi
mul rdi # RDX:RAX = RAX * RDI
mov rax, rdx
ret
(或使用clang -march=haswell 启用BMI2:mov rdx, rsi / mulx rax, rcx, rdi 将高半部分直接放入RAX。gcc 是愚蠢的,仍然使用额外的mov。)
对于 AArch64(使用 gcc unsigned __int128 或使用 __umulh 的 MSVC):
test_var:
umulh x0, x0, x1
ret
使用 2 乘法器的编译时常数幂,我们通常会得到预期的右移来抓取几个高位。但是 gcc 有趣地使用了shld(参见 Godbolt 链接)。
不幸的是,当前的编译器没有优化@craigster0 的可移植版本。你会得到 8x shr r64,32、4x imul r64,r64 和一堆 add/mov x86-64 指令。即它编译成很多 32x32 => 64 位乘法和解包结果。因此,如果您想要利用 64 位 CPU 的功能,您需要一些 #ifdefs。
完全乘法 mul 64 指令在 Intel CPU 上是 2 微秒,但仍然只有 3 个周期延迟,与仅产生 64 位结果的 imul r64,r64 相同。因此,基于http://agner.org/optimize/ 的快速眼球猜测,在现代 x86-64 上,__int128 / 内在版本的延迟和吞吐量(对周围代码的影响)比便携式版本便宜 5 到 10 倍。
在上述链接上的 Godbolt 编译器资源管理器中查看它。
gcc 确实在乘以 16 时完全优化了这个函数:你得到一个右移,比 unsigned __int128 multiply 更有效。