C++ 使用AVX Intrinsic计算允许标量值为0、1和2的向量的内积

C++ 使用AVX Intrinsic计算允许标量值为0、1和2的向量的内积,c++,simd,avx,C++,Simd,Avx,我在做两列数万维的内积。这些值只能为0、1或2。因此,它们可以存储为字符。如果要在带有avx标志的CPU上对计算进行矢量化,我希望它会快约32倍。但问题是乘法会自动将字符转换为整数,即4字节。因此,仅在速度上可获得8倍的最大值。能达到32倍的速度吗 顺便说一句,我正在使用Linux(迄今为止的Fedora 22)和g++5.1。如果您有(不仅仅是AVX,它实际上只用于浮点),那么您可以使用vpmaddubsw指令,其内在特性是: __m256i _mm256_maddubs_epi16 (__m

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

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

如果您有(不仅仅是AVX,它实际上只用于浮点),那么您可以使用
vpmaddubsw
指令,其内在特性是:

__m256i _mm256_maddubs_epi16 (__m256i a, __m256i b)
这将执行8位x 8位乘法(有符号x无符号,但这与您的情况无关),然后添加相邻项对以得到16位结果。这可以有效地在一条指令中实现32 x 8 x 8位乘法

如果没有AVX2,则可以使用128位SSE版本(
\u mm\u maddubs\u epi16
)在一条指令中实现16 x 8 x 8位乘法

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


上面的AVX2实现留给读者作为练习。

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

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

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

0 = 0b00000000 = 0x00
1 = 0b00010011 = 0x13
2 = 0b00001111 = 0x0F
0 = 0b00000000 = 0x00
1 = 0b00011100 = 0x1C
2 = 0b00001111 = 0x0F
在第二个向量
B
中,使用以下编码:

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

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

我想通过位操作来尝试是值得的

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

for (int i = 0; i < N; i += 256)
  res += popcount(A[i..i+255] & B[i..i+255]);
现在我们可以注意到:

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]);
}

答案是肯定的。你的问题是什么?(你甚至没有告诉我们你使用的是哪种操作系统或编译器,这使得很难具体说明。)确切地说,我使用的是Linux,Fedora。编译器是g++5.1。我还拥有英特尔C 2015的许可证。G++是首选。AVX不支持整数乘法。
ymm
。哇,开箱即用,我印象深刻:
\u-mm\u-maddubs\u-epi16
/
\u-mm256\u-maddubs\u-epi16
\u-epi16
后缀有点误导性)。你可以映射0、1和2的向量,通过将其用作
VPSHUFB
的洗牌控制掩码,将此编码设置为无效。(压缩到5比特会更慢)。要洗牌的向量只需要低3个字节的数据,因此您可以使用简单的
VMOVD
加载它。为了完整性,您可以添加横向16位加法的内在值吗?@KrzysztofKosiński:很难有效地进行水平加法,但在这种特殊情况下,只需要非常不频繁地计算它。我将在答案中添加一个注释。您可以打包2位元素,而不是两个独立的位数组。您可以使用shift+和遮罩获得A1和B2。这需要更多的指示。另外,最近的一个问题是询问256b popcount,答案是4x
popcnt r64,r64
(SSE4.2)可能比建议的代码更有效。@PeterCordes将两个数组打包在一起是没有用的。这只会让事情变得更复杂,也许更慢。关于popcount,我同意最快的版本可能是对64位整数调用popcnt 4次。也许我提出的解决方案在没有任何SSE/AVX寄存器的情况下会更快(只需使用64位整数)。不管怎么说,Paul R提出的具有适当内在特性的解决方案似乎更快。打包它们可能对生成输入的任何东西更有用,也可能对查看输出的任何东西更有用。是的,如果要在GP寄存器中使用popcnt,只需对所有内容使用GP regs即可
popcnt
只能在端口1上运行,但Haswell可以在所有四个ALU端口上运行GP reg
/
ADD
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