【问题标题】:SSE multiplication of 2 64-bit integers2 个 64 位整数的 SSE 乘法
【发布时间】:2013-07-25 15:56:44
【问题描述】:

如何将两个 64 位整数乘以另外两个 64 位整数? 我没有找到任何可以做到这一点的指令。

【问题讨论】:

  • 在这种情况下,“两个 64 位整数”是什么意思?您是指一对 64 位整数(复数),还是表示为一对 64 位整数的单个 128 位整数?
  • 我的意思是单个 m128i 位整数表示为一对 64 位整数
  • this question 的可能副本。
  • 相关:Fastest way to multiply an array of int64_t? 用于 AVX2 或 SSE4.1,具有性能分析与 64 位标量代码(如果您的数据还没有 SIMD 向量)。

标签: x86 sse simd multiplication sse2


【解决方案1】:

迟到的答案,但这是巴拉巴斯发布的更好的版本。

如果你曾经使用过 GCC 或 Clang 的向量扩展,这是他们使用的例程。

这使用与长乘法和网格乘法相同的方法。

    65
  * 73
  ----
    15 //   (5 * 3)
   180 //   (6 * 3) * 10
   350 //   (5 * 7) * 10
+ 4200 // + (6 * 7) * 100
------
  4745

但是,它不是使用 10 的每个单位,而是使用 32 位的每个单位,并且省略了最后一个乘法,因为它总是会移到第 64 位之后,就像你不会乘以 6*7如果您要截断大于 99 的值。

#include <emmintrin.h>

/*
 * Grid/long multiply two 64-bit SSE lanes.
 * Works for both signed and unsigned.
 *   ----------------.--------------.----------------.
 *  |                |   b >> 32    | a & 0xFFFFFFFF |
 *  |----------------|--------------|----------------|  
 *  | d >> 32        |   b*d << 64  |    a*d << 32   |
 *  |----------------|--------------|----------------|
 *  | c & 0xFFFFFFFF |   b*c << 32  |       a*c      |
 *  '----------------'--------------'----------------'
 *  Add all of them together to get the product.
 *
 *  Because we truncate the value to 64 bits, b*d << 64 will be zero,
 *  so we can leave it out.
 *
 *  We also can add a*d and b*c first and then shift because of the
 *  distributive property: (a << 32) + (b << 32) == (a + b) << 32.
 */

__m128i Multiply64Bit(__m128i ab, __m128i cd)
{
    /* ac = (ab & 0xFFFFFFFF) * (cd & 0xFFFFFFFF); */
    __m128i ac = _mm_mul_epu32(ab, cd);

    /* b = ab >> 32; */
    __m128i b = _mm_srli_epi64(ab, 32);

    /* bc = b * (cd & 0xFFFFFFFF); */
    __m128i bc = _mm_mul_epu32(b, cd);

    /* d = cd >> 32; */
    __m128i d = _mm_srli_epi64(cd, 32);

    /* ad = (ab & 0xFFFFFFFF) * d; */
    __m128i ad = _mm_mul_epu32(ab, d);

    /* high = bc + ad; */
    __m128i high = _mm_add_epi64(bc, ad);

    /* high <<= 32; */
    high = _mm_slli_epi64(high, 32);

    /* return ac + high; */
    return _mm_add_epi64(high, ac);
}

Compiler Explorer 注意:下面还包含了 GCC 矢量扩展版本以供比较。

【讨论】:

  • 有了-march=skylake-avx512,我们得到了AVX512DQ vpmulqq :) AVX512 终于引入了 64 位元素大小的整数乘法。
  • 顺便说一句,如果没有 AVX2,使用 SIMD 进行 64x64 => 64 位乘法可能不值得,除非您已经将数据保存在向量中。 (每个 64 位整数一个标量 imul r64, r/m64 uop 非常好。Fastest way to multiply an array of int64_t?)。我的回答使用mullo_epi32(SSE4.1 或 AVX2)同时获得两种低*高产品,尽管pmulld 在英特尔 CPU 上确实需要 2 微秒。
  • 是的。我确实想提一下,用于 Neon 的方法也可以做到这一点,它执行 vrev64(32 位 wordswap)、4xi32 乘法、vpaddl(成对加法)、左移,然后长乘法累加。如果 SSE 有成对相加,那可能会更快,但考虑到 NEON_2_SSE 对该指令进行了标量化,我认为它没有。
  • SSSE3 has phaddd,但它解码为 2 次随机播放,提供垂直的 paddd uop;不使用它会更快。我没有查看链接答案的详细信息,但它确实提到了使用 psrlq / paddq / pand (总共 3 个 uops)而不是 phadd + pshufd (3 个 shuffle uops + 一个 ADD)。更多指令但更少的微指令,以及更少的随机端口瓶颈。哦,vpaddl 扩大了元素。 PHADDD 有 2 个输入和 1 个输出,所以不,它不是完全替代品。
【解决方案2】:

我知道这是一个老问题,但我实际上正在寻找这个。由于仍然没有关于它的指令,我实现了 64 位乘法自己与 Paul R 提到的 pmuldq。这是我想出的:

// requires g++ -msse4.1 ...

#include <emmintrin.h>
#include <smmintrin.h>

__m128i Multiply64Bit(__m128i a, __m128i b)
{
    auto ax0_ax1_ay0_ay1 = a;
    auto bx0_bx1_by0_by1 = b;

    // i means ignored

    auto ax1_i_ay1_i = _mm_shuffle_epi32(ax0_ax1_ay0_ay1, _MM_SHUFFLE(3, 3, 1, 1));
    auto bx1_i_by1_i = _mm_shuffle_epi32(bx0_bx1_by0_by1, _MM_SHUFFLE(3, 3, 1, 1));

    auto ax0bx0_ay0by0 = _mm_mul_epi32(ax0_ax1_ay0_ay1, bx0_bx1_by0_by1);
    auto ax0bx1_ay0by1 = _mm_mul_epi32(ax0_ax1_ay0_ay1, bx1_i_by1_i);
    auto ax1bx0_ay1by0 = _mm_mul_epi32(ax1_i_ay1_i, bx0_bx1_by0_by1);

    auto ax0bx1_ay0by1_32 = _mm_slli_epi64(ax0bx1_ay0by1, 32);
    auto ax1bx0_ay1by0_32 = _mm_slli_epi64(ax1bx0_ay1by0, 32);

    return _mm_add_epi64(ax0bx0_ay0by0, _mm_add_epi64(ax0bx1_ay0by1_32, ax1bx0_ay1by0_32));
}

Godbolt SSE Multiply64Bit

【讨论】:

  • 您是否对代码进行了任何基准测试,而不是为此使用通用寄存器?我会对结果感兴趣,因为我正在进行大量 64 x 64 位乘法运算。
  • 我刚刚做了一些基准测试,它仍然比标量乘法(使用 cl /O2 编译)快。平均约 831600000 次乘法。在我功能强大的 i7 5820k 上运行 0.37 秒。同时,相同的标量乘法在 avg 上取 1.71。所以它快了大约 4 倍,这有点奇怪。我猜 cl 真的很擅长优化超标量指令
  • _mm_mul_epi32 是 SSE4.1 指令。 _mm_mul_epu32 是 SSE2 指令。 _mm_mul_epu32 生成更好的代码,但它需要无符号类型。
【解决方案3】:

您需要使用 32 位乘法运算来实现自己的 64 位乘法例程。不过,它可能不会比仅使用标量代码更有效,特别是因为需要对向量进行大量改组才能获得所有必需的操作。

【讨论】:

  • 在我的脑海中,是不是有一个 pmuldqq 或 SSE4 中添加的东西?
  • SSE4 中有一个 pmuldq,它是一个 32x32 => 64 位乘法,因此您可以将其用作 64x64 位乘法的构建块。
  • 你知道最好的标量算法吗(假设你只有 32 位硬件)?这就是我会做的。我会将每个数字分为上下 32 位部分,然后执行 (ab) = (al+ah)*(blbh) = t1 + t2 + t3 + t4 其中 t1= albl, t2=albh, t3=ahbl t4=ahbh。每个术语都是一个 64 位的数字。然后 t2 和 t3 必须再次分为低和高,最终数字将是 (ab)l = t1 + t2l + t3l, (ab)h = t4 + t2h + t3h + c,其中 c 是 (a*b)l 的任何进位。那是 4 乘法和 7 加法。这是在某处吗?
  • 我自己从来没有实现过这个,但它应该是你建议的方法。我无法想象它会非常有效,所以只有当你有其他 64 位 SIMD 操作想要与之结合时才值得。
  • 在 Sandy Bridge 上,通用乘法和向量乘法被发送到不同的端口,因此如果您进行一组以上的乘法运算,您可能能够免费获得 SSE 乘法运算。但是,添加和洗牌将是一个问题。如果您做的事情不需要太多端口 5,这些也可能会免费提供。
猜你喜欢
  • 2012-05-17
  • 2015-07-29
  • 1970-01-01
  • 2012-03-16
  • 2012-04-30
  • 2015-10-17
  • 2015-05-06
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多