获得64位整数乘法的高分

时间:2015-03-05 01:23:25

标签: c++ assembly 64-bit multiplication

在C ++中,请说:

uint64_t i;
uint64_t j;

然后i * j会产生一个uint64_t,其值为ij之间乘法的下半部分,即(i * j) mod 2^64。 现在,如果我想要乘法的较高部分怎么办?我知道在使用32位整数时有一个汇编指令可以做类似的事情,但我对汇编并不熟悉,所以我希望能帮忙。

制作类似内容的最有效方法是什么:

uint64_t k = mulhi(i, j);

5 个答案:

答案 0 :(得分:16)

如果你正在使用gcc并且你所支持的版本支持128位数(尝试使用__uint128_t)而不是执行128位乘法,并且提取高64位可能是获得结果的最有效方式。 / p>

如果您的编译器不支持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位数而不会溢出。但是,上述计算的中间结果不会。

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

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关闭,你可以省略进位的计算。

答案 1 :(得分:2)

长乘法应该是好的。

a*b拆分为(hia+loa)*(hib+lob)。这给出了4个32位乘法加上一些移位。用64位进行,然后手动进行,你将得到很高的部分。

请注意,高部分的近似可以用较少的乘法来完成 - 精确到2 ^ 33左右,1乘法,1到3乘法。

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

答案 2 :(得分:2)

遗憾的是,当前的编译器不要优化@ craigster0的漂亮便携式版本,因此,如果您想利用64位CPU,则只能使用它作为目标的后备,您没有#ifdef的支持。 (我看不到优化它的通用方法;您需要128位类型或内部函数。)


大多数64位平台上的GNU C(gcc,clang或ICC)has unsigned __int128。 (或在较旧的版本中,__uint128_t)。不过,GCC并未在32位平台上实现这种类型。

这是使编译器发出64位全乘法指令并保持高位一半的简单有效的方法。 (GCC知道,强制转换为128位整数的uint64_t的上半部分全为零,因此您不会使用三个64位乘法得到128位乘法。)

MSVC also has a __umulh intrinsic用于64位高半倍乘法,但同样仅在64位平台(特别是x86-64和AArch64)上可用。文档还提到IPF(IA-64)具有{{1 }},但我没有用于Itanium的MSVC。(可能仍然不相关)

_umul128

对于x86-64,AArch64和PowerPC64(及其他),这将编译为一条#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 指令,以及几对mul来处理调用约定(应该在此内联之后进行优化)。  来自the Godbolt compiler explorer(带有x86-64,PowerPC64和AArch64的源+ asm):

mov

(或使用 # 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 来启用BMI2:clang -march=haswell / mov rdx, rsi直接将上半部分放到RAX中。gcc很笨,仍然使用了额外的mulx rax, rcx, rdi。 )

对于AArch64(使用gcc mov或MSVC使用unsigned __int128):

__umulh

使用2乘数的编译时恒定乘方,我们通常会获得预期的右移,以获取一些高位。但是gcc有趣地使用了test_var: umulh x0, x0, x1 ret (请参阅Godbolt链接)。


不幸的是,当前的编译器不要优化@ craigster0的漂亮便携式版本。对于x86-64,您得到8x shld,4x shr r64,32和一堆imul r64,r64 / add指令。即它可以编译为很多32x32 => 64位乘法并解压缩结果。因此,如果您希望利用64位CPU,则需要一些mov

在Intel CPU上,全乘#ifdef指令为2 oups,但与mul 64相同,其仅产生3位周期延迟,后者仅产生64位结果。因此,imul r64,r64 /内在版本在现代x86-64上的延迟和吞吐量(对周围代码的影响)比便携式版本便宜5至10倍,这是基于http://agner.org/optimize/的快速猜测。 / p>

在上述链接的Godbolt编译器资源管理器中进行检查。

gcc在乘以16时会完全优化此功能:与__int128乘积相比,您得到的单向右移效率更高。

答案 3 :(得分:1)

这是我今晚提出的经过单元测试的版本,提供完整的128位产品。在检查时,它似乎比大多数其他在线解决方案更简单(例如在Botan库和此处的其他答案中),因为它利用了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

答案 4 :(得分:-1)

这是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编译器的汇编:

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");