【问题标题】:Getting the high part of 64 bit integer multiplication获取 64 位整数乘法的高位部分
【发布时间】:2015-05-06 06:18:00
【问题描述】:

在 C++ 中,这样说:

uint64_t i;
uint64_t j;

然后i * j 将产生一个uint64_t,它的值是ij 之间的乘积的下半部分,即(i * j) mod 2^64。 现在,如果我想要乘法的较高部分怎么办?我知道在使用 32 位整数时存在类似的汇编指令,但我对汇编一点也不熟悉,所以我希望得到帮助。

什么是最有效的方法来制作类似的东西:

uint64_t k = mulhi(i, j);

【问题讨论】:

标签: c++ assembly 64-bit multiplication


【解决方案1】:

如果您使用 gcc 并且您拥有的版本支持 128 位数字(尝试使用 __uint128_t),那么执行 128 乘法并提取高 64 位可能是获得结果的最有效方法。

如果你的编译器不支持 128 位数字,那么 Yakk 的回答是正确的。但是,对于一般消费来说,它可能太简短了。特别是,实际实现必须小心溢出 64 位整数。

他提出的简单且可移植的解决方案是将 a 和 b 分别分解为 2 个 32 位数字,然后使用 64 位乘法运算将这些 32 位数字相乘。如果我们写:

uint64_t a_lo = (uint32_t)a;
uint64_t a_hi = a >> 32;
uint64_t b_lo = (uint32_t)b;
uint64_t b_hi = b >> 32;

那么很明显:

a = (a_hi << 32) + a_lo;
b = (b_hi << 32) + b_lo;

和:

a * b = ((a_hi << 32) + a_lo) * ((b_hi << 32) + b_lo)
      = ((a_hi * b_hi) << 64) +
        ((a_hi * b_lo) << 32) +
        ((b_hi * a_lo) << 32) +
          a_lo * b_lo

如果计算是使用 128 位(或更高)的算法执行的。

但是这个问题需要我们使用 64 位算术来执行所有的计算,所以我们不得不担心溢出。

由于 a_hi、a_lo、b_hi 和 b_lo 都是无符号 32 位数字,它们的乘积将适合无符号 64 位数字而不会溢出。但是,上述计算的中间结果不会。

当数学必须以 2^64 为模执行时,以下代码将实现 mulhi(a, b):

uint64_t    a_lo = (uint32_t)a;
uint64_t    a_hi = a >> 32;
uint64_t    b_lo = (uint32_t)b;
uint64_t    b_hi = b >> 32;

uint64_t    a_x_b_hi =  a_hi * b_hi;
uint64_t    a_x_b_mid = a_hi * b_lo;
uint64_t    b_x_a_mid = b_hi * a_lo;
uint64_t    a_x_b_lo =  a_lo * b_lo;

uint64_t    carry_bit = ((uint64_t)(uint32_t)a_x_b_mid +
                         (uint64_t)(uint32_t)b_x_a_mid +
                         (a_x_b_lo >> 32) ) >> 32;

uint64_t    multhi = a_x_b_hi +
                     (a_x_b_mid >> 32) + (b_x_a_mid >> 32) +
                     carry_bit;

return multhi;
                                              

正如 Yakk 所指出的,如果你不介意在高 64 位中被 +1,你可以省略进位位的计算。

【讨论】:

    【解决方案2】:

    这是 ARMv8 或 Aarch64 版本的 asm:

    // High (p1) and low (p0) product
    uint64_t p0, p1;
    // multiplicand and multiplier
    uint64_t a = ..., b = ...;
    
    p0 = a*b; asm ("umulh %0,%1,%2" : "=r"(p1) : "r"(a), "r"(b));
    

    这是旧 DEC 编译器的 asm:

    p0 = a*b; p1 = asm("umulh %a0, %a1, %v0", a, b);
    

    如果你有 x86 的 BMI2 并且想使用mulxq

    asm ("mulxq %3, %0, %1" : "=r"(p0), "=r"(p1) : "d"(a), "r"(b));
    

    而通用 x86 使用 mulq 相乘:

    asm ("mulq %3" : "=a"(p0), "=d"(p1) : "a"(a), "g"(b) : "cc");
    

    【讨论】:

    • 这是 GNU C 内联汇编,这意味着您可以改用 unsigned __int128,就像我的回答所示。这有什么用例?某些 GCC 或 clang 版本是否无法为 (a * (unsigned __int128)b) &gt;&gt; 64 发出 umulh?哦,我刚刚看了我的答案,它显示 AArch64 GCC 发出 umulh
    • @Peter - 你没有回答 OP 的问题。他想要手术用的asm;不是 C 代码的反汇编。
    • 显然不是这样的;接受的答案是纯 C++,没有提到 asm 或内联 asm。我强烈建议将来的读者不要为此使用内联 asm,尤其是在 64 位目标上,因此展示如何使用 GNU C 内联 asm 包装 umulh 对我来说似乎完全没用。特别是当我的回答已经表明该指令存在时。
    • 我不认为我在做一厢情愿。我看到我们不同意解释这个问题(和/或对未来读者可能有用的东西)。问题是“但我对组装一点也不熟悉,所以我希望得到帮助。”他们通过询问如何在 C++ 中获得高半部分来避免 XY 问题,将内联 asm 作为 option 他们的东西可能有用,而不是要求。它甚至没有标记[inline-assembly]
    • OP 显然不是专门要求组装的。他们只说他们知道存在这样的程序集,并要求让编译器发出这些指令的最佳方法。十多年来,内联汇编在可以避免的地方一直是不可取的。
    【解决方案3】:

    TL:DR with GCC for a 64-bit ISA: (a * (unsigned __int128)b) &gt;&gt; 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 更有效。

    【讨论】:

    【解决方案4】:

    这是我今晚提出的一个单元测试版本,它提供了完整的 128 位产品。经检查,它似乎比大多数其他在线解决方案(例如 Botan 库和此处的其他答案)更简单,因为它利用了代码 cmets 中解释的 MIDDLE PART 不溢出的方式。

    对于上下文,我为这个 github 项目编写了它:https://github.com/catid/fp61

    //------------------------------------------------------------------------------
    // Portability Macros
    
    // Compiler-specific force inline keyword
    #ifdef _MSC_VER
    # define FP61_FORCE_INLINE inline __forceinline
    #else
    # define FP61_FORCE_INLINE inline __attribute__((always_inline))
    #endif
    
    
    //------------------------------------------------------------------------------
    // Portable 64x64->128 Multiply
    // CAT_MUL128: r{hi,lo} = x * y
    
    // Returns low part of product, and high part is set in r_hi
    FP61_FORCE_INLINE uint64_t Emulate64x64to128(
        uint64_t& r_hi,
        const uint64_t x,
        const uint64_t y)
    {
        const uint64_t x0 = (uint32_t)x, x1 = x >> 32;
        const uint64_t y0 = (uint32_t)y, y1 = y >> 32;
        const uint64_t p11 = x1 * y1, p01 = x0 * y1;
        const uint64_t p10 = x1 * y0, p00 = x0 * y0;
        /*
            This is implementing schoolbook multiplication:
    
                    x1 x0
            X       y1 y0
            -------------
                       00  LOW PART
            -------------
                    00
                 10 10     MIDDLE PART
            +       01
            -------------
                 01 
            + 11 11        HIGH PART
            -------------
        */
    
        // 64-bit product + two 32-bit values
        const uint64_t middle = p10 + (p00 >> 32) + (uint32_t)p01;
    
        /*
            Proof that 64-bit products can accumulate two more 32-bit values
            without overflowing:
    
            Max 32-bit value is 2^32 - 1.
            PSum = (2^32-1) * (2^32-1) + (2^32-1) + (2^32-1)
                 = 2^64 - 2^32 - 2^32 + 1 + 2^32 - 1 + 2^32 - 1
                 = 2^64 - 1
            Therefore it cannot overflow regardless of input.
        */
    
        // 64-bit product + two 32-bit values
        r_hi = p11 + (middle >> 32) + (p01 >> 32);
    
        // Add LOW PART and lower half of MIDDLE PART
        return (middle << 32) | (uint32_t)p00;
    }
    
    #if defined(_MSC_VER) && defined(_WIN64)
    // Visual Studio 64-bit
    
    # include <intrin.h>
    # pragma intrinsic(_umul128)
    # define CAT_MUL128(r_hi, r_lo, x, y) \
        r_lo = _umul128(x, y, &(r_hi));
    
    #elif defined(__SIZEOF_INT128__)
    // Compiler supporting 128-bit values (GCC/Clang)
    
    # define CAT_MUL128(r_hi, r_lo, x, y)                   \
        {                                                   \
            unsigned __int128 w = (unsigned __int128)x * y; \
            r_lo = (uint64_t)w;                             \
            r_hi = (uint64_t)(w >> 64);                     \
        }
    
    #else
    // Emulate 64x64->128-bit multiply with 64x64->64 operations
    
    # define CAT_MUL128(r_hi, r_lo, x, y) \
        r_lo = Emulate64x64to128(r_hi, x, y);
    
    #endif // End CAT_MUL128
    

    【讨论】:

    • (您的 cmets 在代码上方代码下方之间交替。)
    • 我将此移植到C#,它比我遇到的任何其他 64x64 函数都快!
    • 不知道这是否重要,但这在 Aarch64 上会中断。 _umul128 不可用。
    • 有符号乘法怎么样?那是不是只记住符号,做无符号乘法,然后应用符号?
    【解决方案5】:

    长乘法应该是可以的。

    a*b 拆分为(hia+loa)*(hib+lob)。这给出了 4 个 32 位乘法加上一些移位。以 64 位进行,并手动进行进位,您将获得高位。

    请注意,高位部分的近似值可以用更少的乘法来完成——1 次乘法在 2^33 左右精确,3 次乘法在 1 以内。

    我认为没有便携式替代品。

    【讨论】:

    • 为什么不便携?您甚至可以在 C 语言中进行任意精度数学运算,无需任何汇编
    • @luru 我的意思是快速便携的替代品。这基本上是一个最大尺寸很小的 bignum。
    猜你喜欢
    • 1970-01-01
    • 2013-07-25
    • 1970-01-01
    • 1970-01-01
    • 2015-10-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-09
    相关资源
    最近更新 更多