【问题标题】:SIMD signed with unsigned multiplication for 64-bit * 64-bit to 128-bitSIMD 对 64 位 * 64 位到 128 位的无符号乘法进行签名
【发布时间】:2015-05-02 15:50:47
【问题描述】:

我创建了一个使用 SIMD 执行 64 位 * 64 位到 128 位的函数。目前我已经使用 SSE2(实际上是 SSE4.1)实现了它。这意味着它同时做两个 64b*64b 到 128b 的产品。相同的想法可以扩展到 AVX2 或 AVX512,同时提供四个或八个 64b*64 到 128b 产品。 我的算法基于http://www.hackersdelight.org/hdcodetxt/muldws.c.txt

该算法执行一次无符号乘法、一次有符号乘法和两次有符号 * 无符号乘法。使用_mm_mul_epi32_mm_mul_epu32 可以轻松完成有符号* 有符号和无符号* 无符号操作。但是混合签名和未签名的产品给我带来了麻烦。 例如考虑一下。

int32_t x = 0x80000000;
uint32_t y = 0x7fffffff;
int64_t z = (int64_t)x*y;

双字积应该是0xc000000080000000。但是如果你假设你的编译器确实知道如何处理混合类型,你怎么能得到这个呢?这是我想出的:

int64_t sign = x<0; sign*=-1;        //get the sign and make it all ones
uint32_t t = abs(x);                 //if x<0 take two's complement again
uint64_t prod = (uint64_t)t*y;       //unsigned product
int64_t z = (prod ^ sign) - sign;    //take two's complement based on the sign

使用 SSE 可以这样做

__m128i xh;    //(xl2, xh2, xl1, xh1) high is signed, low unsigned
__m128i yl;    //(yh2, yl2, yh2, yl2)
__m128i xs     = _mm_cmpgt_epi32(_mm_setzero_si128(), xh); // get sign
        xs     = _mm_shuffle_epi32(xs, 0xA0);              // extend sign
__m128i t      = _mm_sign_epi32(xh,xh);                    // abs(xh)
__m128i prod   = _mm_mul_epu32(t, yl);                     // unsigned (xh2*yl2,xh1*yl1)
__m128i inv    = _mm_xor_si128(prod,xs);                   // invert bits if negative
__m128i z      = _mm_sub_epi64(inv,xs);                    // add 1 if negative

这给出了正确的结果。但是我必须这样做两次(一次平方时),现在它是我功能的重要组成部分。使用 SSE4.2、AVX2(四个 128 位产品)甚至 AVX512(八个 128 位产品)是否有更有效的方法?

也许有比 SIMD 更有效的方法?要得到大字,需要大量的计算。

编辑:根据@ElderBug 的评论,看起来这样做的方法不是使用 SIMD,而是使用 mul 指令。对于它的价值,如果有人想看看这有多复杂,这里是完整的工作功能(我刚刚开始工作,所以我没有优化它,但我认为它不值得)。

void muldws1_sse(__m128i x, __m128i y, __m128i *lo, __m128i *hi) {
    __m128i lomask = _mm_set1_epi64x(0xffffffff);

    __m128i xh     = _mm_shuffle_epi32(x, 0xB1);    // x0l, x0h, x1l, x1h
    __m128i yh     = _mm_shuffle_epi32(y, 0xB1);    // y0l, y0h, y1l, y1h

    __m128i xs     = _mm_cmpgt_epi32(_mm_setzero_si128(), xh);
    __m128i ys     = _mm_cmpgt_epi32(_mm_setzero_si128(), yh);
            xs     = _mm_shuffle_epi32(xs, 0xA0);
            ys     = _mm_shuffle_epi32(ys, 0xA0);

    __m128i w0     = _mm_mul_epu32(x,  y);          // x0l*y0l, y0l*y0h
    __m128i w3     = _mm_mul_epi32(xh, yh);         // x0h*y0h, x1h*y1h
            xh     = _mm_sign_epi32(xh,xh);
            yh     = _mm_sign_epi32(yh,yh);

    __m128i w1     = _mm_mul_epu32(x,  yh);         // x0l*y0h, x1l*y1h
    __m128i w2     = _mm_mul_epu32(xh, y);          // x0h*y0l, x1h*y0l

    __m128i yinv   = _mm_xor_si128(w1,ys);          // invert bits if negative
            w1     = _mm_sub_epi64(yinv,ys);         // add 1
    __m128i xinv   = _mm_xor_si128(w2,xs);          // invert bits if negative
            w2     = _mm_sub_epi64(xinv,xs);         // add 1

    __m128i w0l    = _mm_and_si128(w0, lomask);
    __m128i w0h    = _mm_srli_epi64(w0, 32);

    __m128i s1     = _mm_add_epi64(w1, w0h);         // xl*yh + w0h;
    __m128i s1l    = _mm_and_si128(s1, lomask);      // lo(wl*yh + w0h);
    __m128i s1h    = _mm_srai_epi64(s1, 32);

    __m128i s2     = _mm_add_epi64(w2, s1l);         //xh*yl + s1l
    __m128i s2l    = _mm_slli_epi64(s2, 32);
    __m128i s2h    = _mm_srai_epi64(s2, 32);           //arithmetic shift right

    __m128i hi1    = _mm_add_epi64(w3, s1h);
            hi1    = _mm_add_epi64(hi1, s2h);

    __m128i lo1    = _mm_add_epi64(w0l, s2l);
    *hi = hi1;
    *lo = lo1;
}

情况变得更糟。在 AVX512 之前没有 _mm_srai_epi64 instrinsic/instruction,所以我必须自己制作。

static inline __m128i _mm_srai_epi64(__m128i a, int b) {
    __m128i sra = _mm_srai_epi32(a,32);
    __m128i srl = _mm_srli_epi64(a,32);
    __m128i mask = _mm_set_epi32(-1,0,-1,0);
    __m128i out = _mm_blendv_epi8(srl, sra, mask);
}

我上面的_mm_srai_epi64 的实现是不完整的。我想我使用的是 Agner Fog 的Vector Class Library。如果你查看文件 vectori128.h 你会发现

static inline Vec2q operator >> (Vec2q const & a, int32_t b) {
    // instruction does not exist. Split into 32-bit shifts
    if (b <= 32) {
        __m128i bb   = _mm_cvtsi32_si128(b);               // b
        __m128i sra  = _mm_sra_epi32(a,bb);                // a >> b signed dwords
        __m128i srl  = _mm_srl_epi64(a,bb);                // a >> b unsigned qwords
        __m128i mask = _mm_setr_epi32(0,-1,0,-1);          // mask for signed high part
        return  selectb(mask,sra,srl);
    }
    else {  // b > 32
        __m128i bm32 = _mm_cvtsi32_si128(b-32);            // b - 32
        __m128i sign = _mm_srai_epi32(a,31);               // sign of a
        __m128i sra2 = _mm_sra_epi32(a,bm32);              // a >> (b-32) signed dwords
        __m128i sra3 = _mm_srli_epi64(sra2,32);            // a >> (b-32) >> 32 (second shift unsigned qword)
        __m128i mask = _mm_setr_epi32(0,-1,0,-1);          // mask for high part containing only sign
        return  selectb(mask,sign,sra3);
    }
}

【问题讨论】:

  • 你不能只使用汇编mul指令吗?它已经做了 64*64 = 128 乘法,而且是一条指令。
  • @ElderBug,是的,I just learned that it does this。也许我在浪费时间。使用 AVX512,我可以一次做 8 个 128 产品。我不知道这是否会击败mul 八次。 AVX512 有_mm512_mullo_epi64,但遗憾的是没有_mm512_mul_epi64
  • 这是关于乘法的,所以我认为英特尔 ADX 不会在这里工作,但 BMI2 中的 MULX 可能会稍微提高性能。由于您想提高计算的精度,您可能会对double-double arithmetics 感兴趣。它具有更宽的动态范围并且更容易矢量化,因为您不再需要从低部分进行携带,并且乘法也可能更容易。缺点是精度限制在 106/107 位
  • @Zboson:如果您愿意对边缘情况采取轻松的态度,那么 double-double 在 Haswell 及其他平台(感谢 FMA)上的 SIMD 中非常容易实现(而且速度非常快)。如果您需要正确处理 NaN 和 inf,那就有点痛苦了。您肯定希望将高位和低位部分保存在单独的寄存器中,而不是交错的。
  • C(带有一些 ppc 内联 asm)双双的实现:opensource.apple.com/source/gcc/gcc-5646/gcc/config/rs6000/…

标签: c x86 integer bit-manipulation sse


【解决方案1】:

考虑使用各种指令的整数乘法的吞吐量限制的正确方法是每个周期可以计算多少“乘积位”。

mulx 每个周期产生一个 64x64 -> 128 结果;那是 64x64 = 4096 “每周期的产品位数”

如果您从执行 32x32 -> 64 位乘法的指令中拼凑出 SIMD 上的乘法器,则您需要能够在每个周期获得四个结果以匹配 mulx (4x32x32 = 4096)。如果除了乘法之外没有算术,那么您将在 AVX2 上收支平衡。不幸的是,正如您所注意到的,除了乘法之外还有很多算术运算,所以这在当前一代硬件上完全不是入门。

【讨论】:

  • 让我开始做这件事的是_mm512_mullo_epi64。我想我可以用它来快速获得大字。 But it turns out that getting the upper world given the lower word is hardly easier(确定 0、1 或 2 的进位需要大量工作,更不用说符号*无符号项了)。我对此感到非常惊讶。因为英特尔没有创建_mm512_mul_epi64,80 年代 16 位标量指令的扩展胜过 2015 年最好的向量指令。
  • @Zboson 我不会指望_mm512_mullo_epi64 是一个快速指令。英特尔添加了 IFMA52 指令(可能是基于他们的模拟器的 Cannonlake)这一事实表明,英特尔无意在短期内将 64 位乘法器放入他们的 SIMD 中。
  • @Zboson 此外,获得 unsigned 64x64 乘法的上半部分要容易得多。我用 AVX2 实现了这个。在我的特定应用程序的上下文中,它比将它们拉出到标量 mulx 并重新打包它们快 40%。
  • @Mysticial,我发布了我的问题的答案,它使用更简单的无符号 64x64 到 128 来构建签名产品。我认为它比我以前的要好得多。我的未签名函数 muldwu1_sse 和你的 AVX2 版本相似吗?
  • @Mysticial,事实证明,如果有可以执行 64x64 到 64 的指令,签名版本应该采用与未签名版本相同数量的指令。所以在 AVX512 上可以实现签名版本只使用 16 条指令,就像我对未签名版本所做的那样。但是,正如您所说,64x64 到 64 指令可能比 32x32 到 64 指令慢得多,因此在有符号版本中执行 3 次可能比使用无符号和更正要慢得多。
【解决方案2】:

我找到了一个简单得多的 SIMD 解决方案,不需要signed*unsigned 产品。 我不再相信 SIMD(至少与 AVX2 和 AV512)无法与 mulx 竞争。 在某些情况下,SIMD 可以与 mulx 竞争。我知道的唯一案例是FFT based multiplication of large numbers

诀窍是先进行无符号乘法,然后再正确。我从这个答案32-bit-signed-multiplication-without-using-64-bit-data-type 中学会了如何做到这一点。 (hi,lo) = x*y 的更正很简单,先进行无符号乘法,然后更正hi,如下所示:

hi -= ((x<0) ? y : 0)  + ((y<0) ? x : 0)

这可以通过 SSE4.2 内在 _mm_cmpgt_epi64 来完成

void muldws1_sse(__m128i x, __m128i y, __m128i *lo, __m128i *hi) {    
    muldwu1_sse(x,y,lo,hi);    
    //hi -= ((x<0) ? y : 0)  + ((y<0) ? x : 0);
    __m128i xs = _mm_cmpgt_epi64(_mm_setzero_si128(), x);
    __m128i ys = _mm_cmpgt_epi64(_mm_setzero_si128(), y);           
    __m128i t1 = _mm_and_si128(y,xs);
    __m128i t2 = _mm_and_si128(x,ys);
           *hi = _mm_sub_epi64(*hi,t1);
           *hi = _mm_sub_epi64(*hi,t2);
}

无符号乘法的代码更简单,因为它不需要混合的signed*unsigned 乘积。此外,由于它是无符号的,它不需要算术右移,它只有一条针对 AVX512 的指令。其实下面的函数只需要SSE2:

void muldwu1_sse(__m128i x, __m128i y, __m128i *lo, __m128i *hi) {    
    __m128i lomask = _mm_set1_epi64x(0xffffffff);

    __m128i xh     = _mm_shuffle_epi32(x, 0xB1);    // x0l, x0h, x1l, x1h
    __m128i yh     = _mm_shuffle_epi32(y, 0xB1);    // y0l, y0h, y1l, y1h

    __m128i w0     = _mm_mul_epu32(x,  y);          // x0l*y0l, x1l*y1l
    __m128i w1     = _mm_mul_epu32(x,  yh);         // x0l*y0h, x1l*y1h
    __m128i w2     = _mm_mul_epu32(xh, y);          // x0h*y0l, x1h*y0l
    __m128i w3     = _mm_mul_epu32(xh, yh);         // x0h*y0h, x1h*y1h

    __m128i w0l    = _mm_and_si128(w0, lomask);     //(*)
    __m128i w0h    = _mm_srli_epi64(w0, 32);

    __m128i s1     = _mm_add_epi64(w1, w0h);
    __m128i s1l    = _mm_and_si128(s1, lomask);
    __m128i s1h    = _mm_srli_epi64(s1, 32);

    __m128i s2     = _mm_add_epi64(w2, s1l);
    __m128i s2l    = _mm_slli_epi64(s2, 32);        //(*)
    __m128i s2h    = _mm_srli_epi64(s2, 32);

    __m128i hi1    = _mm_add_epi64(w3, s1h);
            hi1    = _mm_add_epi64(hi1, s2h);

    __m128i lo1    = _mm_add_epi64(w0l, s2l);       //(*)
    //__m128i lo1    = _mm_mullo_epi64(x,y);          //alternative

    *hi = hi1;
    *lo = lo1;
}

这个使用

4x mul_epu32
5x add_epi64
2x shuffle_epi32
2x and
2x srli_epi64
1x slli_epi64
****************
16 instructions

AVX512 具有 _mm_mullo_epi64 内在函数,可以用一条指令计算 lo。在这种情况下,可以使用替代(用 (*) 注释注释行并取消注释替代行):

5x mul_epu32
4x add_epi64
2x shuffle_epi32
1x and
2x srli_epi64
****************
14 instructions

要更改全宽 AVX2 的代码,请将 _mm 替换为 _mm256 ,将 si128 替换为 si256,将 __m128i 替换为 __m256i 对于 AVX512 将它们替换为 _mm512si512,以及__m512i.

【讨论】:

  • 是的,您的无符号 64x64 乘法与我的相似。我的 SSE4.1/AVX2 版本是 19 条指令,但由于移位使用与乘法器相同的端口,因此它将移位换成随机播放。
  • 而我的AVX512版本是15条指令。
  • @Mysticial,很高兴知道。那我就在正确的轨道上。我还没有尝试优化函数(我什至还没有进行优化编译——主要是单元测试)。我应该通过 IACA 运行它。您为什么对多字产品感兴趣?这是用于pi 计算吗?
  • 我没有尝试过,但我不确定它是否会有所帮助。获得完整的 104 位产品需要两条 IFMA 指令。而且您仍然需要其中的 4 个才能达到 128 位。更不用说需要额外的掩蔽和移位了。
  • 很明显,它暴露了双精度电路。这也是我相信_mm512_mullo_epi64() 不会成为快速指令的原因。至少不是通过 Skylake/Cannonlake。
猜你喜欢
  • 1970-01-01
  • 2014-08-25
  • 2010-12-24
  • 2022-01-13
  • 2018-07-18
  • 1970-01-01
  • 2011-01-17
  • 2015-10-17
  • 1970-01-01
相关资源
最近更新 更多