使用AVX内在函数计算允许标量值为0,1和2的向量的内积

时间:2015-07-13 11:30:46

标签: c++ simd avx

我正在做几万维的两列维度的内在产品。值只能是0,1或2.因此可以将它们存储为字符。如果要在具有avx标志的CPU上进行矢量化计算,我预计它会快〜32倍。但问题是乘法会自动将字符转换为整数,即4个字节。因此,可以获得仅速度的最大8倍。可以达到32倍的速度吗?

顺便说一句,我使用的是Linux(Fedora 22迄今为止)和g ++ 5.1。

3 个答案:

答案 0 :(得分:12)

假设您有AVX2(不仅仅是AVX,它只是浮​​点数),那么您可以使用vpmaddubsw指令,其内在函数为:

__m256i _mm256_maddubs_epi16 (__m256i a, __m256i b)

执行8位x 8位乘法(带符号x无符号,但这对您的情况无关紧要),然后添加相邻项对,得到16位结果。 [1]这有效地为您提供了一条指令中的32 x 8 x 8位乘法。

如果您没有AVX2,则可以使用128位SSE版本(_mm_maddubs_epi16)在一条指令中获得16 x x 8 x 8位的乘法。

请注意,对16位项进行水平求和可能需要多条指令,但由于输入范围非常小,因此您只需要相对不频繁地执行此水平求和。一种可能的方法(对于SSE):

v = _mm_madd_epi16(v, _mm_set1_epi16(1));       // unpack/sum 16 -> 32
v = _mm_add_epi32(v, _mm_srli_si128(v, 8));     // shift and add 32 bit terms
v = _mm_add_epi32(v, _mm_srli_si128(v, 4));
sum = _mm_cvtsi128_si32(v);                     // extract sum as scalar

以上的AVX2实现留给读者练习。

答案 1 :(得分:8)

看起来AVX指令集没有8位乘法,只有加法。 The Intel intrinsics guide不包含以_mm_mul*开头的任何8位操作。 (编辑:实际上有一个8位乘法,但它有一个误导性的名称 - 请参阅@PaulR的答案)

然而,还有另一种方法。由于唯一允许的值为0,1和2,并且您正在计算内积,因此可以使用位操作而不是乘法。

在第一个向量A中,使用以下编码:

0 = 0b00000000 = 0x00
1 = 0b00010011 = 0x13
2 = 0b00001111 = 0x0F

在第二个向量B中,使用以下编码:

0 = 0b00000000 = 0x00
1 = 0b00011100 = 0x1C
2 = 0b00001111 = 0x0F

现在计算popcount(A & B)。 AND-ing将使相应的8位单元设置为0,1,2或4位,popcount将它们加在一起。您可以为每5位整数打包一个值,因此如果您可以使用256位整数,则可以获得高51倍的吞吐量。

答案 2 :(得分:2)

我想通过位操作来尝试这样做是值得的。

假设所有数字都是0或1。 然后,您可以将两个向量打包到位数组中。然后通过以下方式计算内积:

for (int i = 0; i < N; i += 256)
  res += popcount(A[i..i+255] & B[i..i+255]);

操作自然存在于AVX / AVX2中。最难的问题是如何快速计算YMM寄存器的popcount。

现在假设我们给出0,1和2.对于每个向量 A 的整数组成两个位向量 A1 A2

A1[i] = (A[i] >= 1);    
A2[i] = (A[i] >= 2);

现在我们可以注意到:

A[i] * B[i] = A1[i] * B1[i] + A1[i] * B2[i] + A2[i] * B1[i] + A2[i] * B2[i];

因此我们可以使用以下伪代码计算内积:

for (int i = 0; i < N; i += 256) {
  res += popcount(A1[i..i+255] & B1[i..i+255]);
  res += popcount(A2[i..i+255] & B1[i..i+255]);
  res += popcount(A1[i..i+255] & B2[i..i+255]);
  res += popcount(A2[i..i+255] & B2[i..i+255]);
}

这允许每次迭代处理256个元素,但每次迭代变慢4倍。有效地,每个操作64个元素。由于popcount可能是计算中最慢的部分,我们可以说它需要N / 64 popcount_256操作来计算内部产品。

编辑:我决定为这个想法添加一个小例子:

A = {01212012210};  //input array A
B = {21221100120};  //input array B
A1 = {01111011110};  //A should be stored in two halves like this
A2 = {00101001100};
B1 = {11111100110};  //B is stored in similar two halves
B2 = {10110000010};
A1 & B1 = {01111000110}, popcount = 6;  //computing pairwise and-s + popcounts
A1 & B2 = {00110000010}, popcount = 3;
A2 & B1 = {00101000100}, popcount = 3;
A2 & B2 = {00100000000}, popcount = 1;
res = 6 + 3 + 3 + 1 = 13   //summing all the popcounts