【问题标题】:How can I multiply 64 bit operands and get 128 bit result portably?如何将 64 位操作数相乘并获得 128 位结果?
【发布时间】:2014-08-02 13:53:04
【问题描述】:

对于 x64,我可以使用这个:

 {
   uint64_t hi, lo;
  // hi,lo = 64bit x 64bit multiply of c[0] and b[0]

   __asm__("mulq %3\n\t"
    : "=d" (hi),
  "=a" (lo)
    : "%a" (c[0]),
  "rm" (b[0])
    : "cc" );

   a[0] += hi;
   a[1] += lo;
 }

但我想便携地执行相同的计算。例如在 x86 上工作。

【问题讨论】:

  • c[0] 和 b[0] 的类型是什么?为什么不将两个 uint64_t 类型相乘?
  • 有什么问题?问题是什么?
  • mulq 是问题所在的 64 位指令,而 c&b 是 uint64_t
  • 如果是C而不是C++,你为什么要标记C++这个问题?很难理解为什么要使用 asm 来执行微不足道的乘法。我也无法理解你的问题。我不知道你在问什么。
  • @DavidHeffernan 感谢清理工作!

标签: c gcc assembly


【解决方案1】:

据我了解,您需要一个 64 位乘法的可移植纯 C 实现,输出为 128 位值,存储在两个 64 位值中。在这种情况下,article 声称拥有您需要的东西。该代码是为 C++ 编写的。转成C代码不需要太多:

void mult64to128(uint64_t op1, uint64_t op2, uint64_t *hi, uint64_t *lo)
{
    uint64_t u1 = (op1 & 0xffffffff);
    uint64_t v1 = (op2 & 0xffffffff);
    uint64_t t = (u1 * v1);
    uint64_t w3 = (t & 0xffffffff);
    uint64_t k = (t >> 32);

    op1 >>= 32;
    t = (op1 * v1) + k;
    k = (t & 0xffffffff);
    uint64_t w1 = (t >> 32);

    op2 >>= 32;
    t = (u1 * op2) + k;
    k = (t >> 32);

    *hi = (op1 * op2) + w1 + k;
    *lo = (t << 32) + w3;
}

【讨论】:

    【解决方案2】:

    由于您有gcc 作为标签,请注意您可以只使用gcc 的128 位整数类型:

    typedef unsigned __int128 uint128_t;
    // ...
    uint64_t x, y;
    // ...
    uint128_t result = (uint128_t)x * y;
    uint64_t lo = result;
    uint64_t hi = result >> 64;
    

    【讨论】:

    【解决方案3】:

    在我看来,公认的解决方案并不是最好的解决方案。

    • 读起来很混乱。
    • 它有一些时髦的携带处理。
    • 它没有利用 64 位算术可用这一事实。
    • 这让 ARMv6 不悦,绝对荒谬的乘法之神。使用UMAAL 的人不会落后,而是在 4 条指令中拥有永恒的 64 位到 128 位乘法。

    除了开玩笑,针对 ARMv6 进行优化比任何其他平台都要好得多,因为它会带来最大的好处。 x86 需要一个复杂的例程,这将是一个死胡同。

    我发现(并在xxHash3 中使用)的最佳方法是这样,它利用了使用宏的多个实现:

    它比 x86 上的 mult64to128 慢一点(1-2 条指令),但在 ARMv6 上快很多。

    #include <stdint.h>
    #ifdef _MSC_VER
    #  include <intrin.h>
    #endif
    
    /* Prevents a partial vectorization from GCC. */
    #if defined(__GNUC__) && !defined(__clang__) && defined(__i386__)
      __attribute__((__target__("no-sse")))
    #endif
    static uint64_t multiply64to128(uint64_t lhs, uint64_t rhs, uint64_t *high)
    {
        /*
         * GCC and Clang usually provide __uint128_t on 64-bit targets,
         * although Clang also defines it on WASM despite having to use
         * builtins for most purposes - including multiplication.
         */
    #if defined(__SIZEOF_INT128__) && !defined(__wasm__)
        __uint128_t product = (__uint128_t)lhs * (__uint128_t)rhs;
        *high = (uint64_t)(product >> 64);
        return (uint64_t)(product & 0xFFFFFFFFFFFFFFFF);
    
        /* Use the _umul128 intrinsic on MSVC x64 to hint for mulq. */
    #elif defined(_MSC_VER) && defined(_M_IX64)
    #   pragma intrinsic(_umul128)
        /* This intentionally has the same signature. */
        return _umul128(lhs, rhs, high);
    
    #else
        /*
         * Fast yet simple grade school multiply that avoids
         * 64-bit carries with the properties of multiplying by 11
         * and takes advantage of UMAAL on ARMv6 to only need 4
         * calculations.
         */
    
        /* First calculate all of the cross products. */
        uint64_t lo_lo = (lhs & 0xFFFFFFFF) * (rhs & 0xFFFFFFFF);
        uint64_t hi_lo = (lhs >> 32)        * (rhs & 0xFFFFFFFF);
        uint64_t lo_hi = (lhs & 0xFFFFFFFF) * (rhs >> 32);
        uint64_t hi_hi = (lhs >> 32)        * (rhs >> 32);
    
        /* Now add the products together. These will never overflow. */
        uint64_t cross = (lo_lo >> 32) + (hi_lo & 0xFFFFFFFF) + lo_hi;
        uint64_t upper = (hi_lo >> 32) + (cross >> 32)        + hi_hi;
    
        *high = upper;
        return (cross << 32) | (lo_lo & 0xFFFFFFFF);
    #endif /* portable */
    }
    

    在 ARMv6 上,没有比这更好的了,至少在 Clang 上:

    multiply64to128:
            push    {r4, r5, r11, lr}
            umull   r12, r5, r2, r0
            umull   r2, r4, r2, r1
            umaal   r2, r5, r3, r0
            umaal   r4, r5, r3, r1
            ldr     r0, [sp, #16]
            mov     r1, r2
            strd    r4, r5, [r0]
            mov     r0, r12
            pop     {r4, r5, r11, pc}
    

    由于 instcombine 错误,已接受的解决方案会在 Clang 中生成一堆 addsadc,以及额外的 umull

    我在我发布的链接中进一步解释了便携式方法。

    【讨论】:

    • __attribute__((__target__("no-sse"))) 可能会阻止它内联到具有不同目标选项的函数中,这可能会破坏常量传播以及增加调用/调用开销(尤其是在讨厌的堆栈参数调用约定中,大多数 32-位码使用)。但这仅适用于 32 位 x86,因此它可能不会损害许多用例。
    • 确实如此。然而,从 Sandy Bridge 上的测试来看,shuffle 完全成为算法的瓶颈。
    • 您是否向 gcc 的 bugzilla 报告了错过优化的错误?我只是指出解决方法并不完美,但如果有一种方法可以在不使用 -fno-tree-vectorize 的情况下对整个文件使用更便宜的方法,那么 IDK。如果-O3 -march=native 踢得那么厉害,你的属性可能是最好的选择。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-11-07
    • 2015-10-17
    • 1970-01-01
    • 1970-01-01
    • 2020-04-24
    • 2013-09-18
    • 2015-05-02
    相关资源
    最近更新 更多