【发布时间】: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