c++ - 是否可以使用 Wojciech Mula 算法对 __m256i 进行 popcount 并将结果存储在 8 个 32 位字而不是 4 个 64 位字中?

标签 c++ intel sse avx avx2


我最近发现 AVX2 没有 __m256i 的 popcount,我发现做类似事情的唯一方法是遵循 Wojciech Mula 算法:

__m256i count(__m256i v) {
    __m256i lookup = _mm256_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2,
                     2, 3, 2, 3, 3, 4, 0, 1, 1, 2, 1, 2, 2, 3,
                     1, 2, 2, 3, 2, 3, 3, 4);
    __m256i low_mask = _mm256_set1_epi8(0x0f);
    __m256i lo =_mm256_and_si256(v,low_mask);
    __m256i hi = _mm256_and_si256( _mm256_srli_epi32(v, 4), low_mask);
    __m256i popcnt1 = _mm256_shuffle_epi8(lookup,lo);
    __m256i popcnt2 = _mm256_shuffle_epi8(lookup,hi);
    __m256i total = _mm256_add_epi8(popcnt1,popcnt2);

    return _mm256_sad_epu8(total,_mm256_setzero_si256());
}

Wojciech Muła, Nathan Kurz, Daniel Lemire, Faster Population Counts Using AVX2 Instructions, Computer Journal 61 (1), 2018

问题是它返回的是 8 个 short 到 long 的总和,而不是 4 个 short 到 int 的总和。

当前发生的事情:
我有 __m256i x,其中包含这 8 个 32 位整数:

  1. 01101011111000011100000000000000
  2. 01110101011010010111100000000000
  3. 10100100011011000101010000000000
  4. 11101010100001001111000000000000
  5. 10010011111111001001010000000000
  6. 00011110101100101000000000000000
  7. 00011101011000111011000000000000
  8. 10011011100010100000110000000000

__m256i res = count(x);

资源包含:

  1. 24
  2. 21
  3. 22
  4. 21

结果是4长64位

期望:

我有 __m256i x,其中包含这 8 个 32 位整数:

  1. 01101011111000011100000000000000
  2. 01110101011010010111100000000000
  3. 10100100011011000101010000000000
  4. 11101010100001001111000000000000
  5. 10010011111111001001010000000000
  6. 00011110101100101000000000000000
  7. 00011101011000111011000000000000
  8. 10011011100010100000110000000000

__m256i res = count(x);

资源包含:

  1. 11
  2. 13
  3. 10
  4. 11
  5. 12
  6. 9
  7. 11
  8. 10

结果是 8 int 32 位。

希望我说的很清楚,请不要犹豫向我询问更精确的信息。

谢谢。

最佳答案

AVX-512VPOPCNTDQ 有 _mm256_popcnt_epi32在 32 位 block 中弹出计数,也是 64 位 block 大小版本。在 Xeon Phi 之外,它是 new in Ice Lake它还引入了 AVX512BITALG,它也具有 vpopcnt 的字节和字(16 位) block 大小。


使用 AVX2

您引用的原始代码依赖于 _mm256_sad_epu8 内在函数,它专门用于对 64 位字内的字节求和。

要使用 32 位字的总和获得相同的结果,您需要做一些稍微不同的事情。以下应该有效:

__m256i popcount_pshufb32(__m256i v) {

  __m256i lookup = _mm256_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2,
                 2, 3, 2, 3, 3, 4, 0, 1, 1, 2, 1, 2, 2, 3,
                 1, 2, 2, 3, 2, 3, 3, 4);
  __m256i low_mask = _mm256_set1_epi8(0x0f);
  __m256i lo = _mm256_and_si256(v, low_mask);
  __m256i hi = _mm256_and_si256(_mm256_srli_epi16(v, 4), low_mask);
  __m256i popcnt1 = _mm256_shuffle_epi8(lookup, lo);
  __m256i popcnt2 = _mm256_shuffle_epi8(lookup, hi);
  __m256i sum8 = _mm256_add_epi8(popcnt1, popcnt2);
  return _mm256_srli_epi32(
      _mm256_mullo_epi32(sum8, _mm256_set1_epi32(0x01010101)), 24);
      // vpmulld is slowish (2 uops) on most recent Intel CPUs
      // but still single-uop on AMD
}

所以我们用乘法和移位替换了 _mm256_sad_epu8。这应该是合理的。在我的测试中,it is slightly slower than the original 64-bit version, but the difference is relatively small .

通过使用不同的两条指令从字节累加到 32 位 block ,您可以以多一个 vector 常量为代价在 Intel 上获得稍微更好的性能。 AMD Zen1/2/3 至少和上面的版本一样高效。

32 位 SIMD 整数乘法在最近的 Intel CPU 上是 2 微指令(对于 SIMD 整数乘法单元),但是成对的乘法累加指令(8->16 和 16->32)是单个每个。 ( https://uops.info/ ) 这需要一个更多的常量,但相同数量的指令,用于更少的 uops,特别是如果编译器可以在循环中重用这些常量。

__m256i popcount_pshufb32(__m256i v) {

  __m256i lookup = _mm256_setr_epi8(0, 1, 1, 2, 1, 2, 2, 3, 1, 2,
                 2, 3, 2, 3, 3, 4, 0, 1, 1, 2, 1, 2, 2, 3,
                 1, 2, 2, 3, 2, 3, 3, 4);
  __m256i low_mask = _mm256_set1_epi8(0x0f);
  __m256i lo = _mm256_and_si256(v, low_mask);
  __m256i hi = _mm256_and_si256(_mm256_srli_epi16(v, 4), low_mask);
  __m256i popcnt1 = _mm256_shuffle_epi8(lookup, lo);
  __m256i popcnt2 = _mm256_shuffle_epi8(lookup, hi);
  __m256i sum8 = _mm256_add_epi8(popcnt1, popcnt2);
  return _mm256_madd_epi16(_mm256_maddubs_epi16(sum8, _mm256_set1_epi8(1)),
                       _mm256_set1_epi16(1));
}

关于c++ - 是否可以使用 Wojciech Mula 算法对 __m256i 进行 popcount 并将结果存储在 8 个 32 位字而不是 4 个 64 位字中?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/51104493/

相关文章:

c++ - 如何将值列表传递给需要数组的函数?

c++ - C/C++/Objective-C 中的复式记账

c - 结构的内存对齐

c - 对 4 个整数大小的数组进行 SSE 操作

gcc - 较高级别的 SSE 标志是否意味着 GCC/clang 中较低级别的标志?

c++ - GetPixel 返回不正确的值

c++ - 避免 dynamic_cast 缓慢的著名解决方案?

assembly - Intel x86 汇编 - lt、gt、eq

c - 刷新写入内存 Controller 的缓冲区到 DDR 设备

c++ - SSE、内在函数和对齐