【问题标题】:Is it possible to popcount __m256i and store result in 8 32-bit words instead of the 4 64-bit using Wojciech Mula algorithm's?是否可以 popcount __m256i 并将结果存储在 8 个 32 位字中,而不是使用 Wojciech Mula 算法的 4 个 64 位?
【发布时间】:2023-12-03 13:28:01
【问题描述】:


我最近发现 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 位 int:

  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 位 int:

  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 位。

希望我说的很清楚,不要犹豫,要求我更准确。

谢谢。

【问题讨论】:

标签: c++ intel sse avx avx2


【解决方案1】:

您引用的原始代码依赖于 _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);
}

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

您可以以使用更多内在函数为代价获得更好的性能:

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

【讨论】:

  • 您确定_mm256_mul_epi32 会起作用吗?它忽略了 32-63 位。我希望_mm256_mullo_epi32 而不是_mm256_mul_epi32。请注意,_mm256_mullo_epi32 在 Intel 上非常慢。使用 shift-add-shift-add-mask_0xFF 的水平和可能一样快。
  • 在仔细阅读 Agner Fog 的指令表之后,我认为乘法的想法可能比重复移位和加法更快,至少对于 Intel 而言,也许对于 AMD Ryzen 而言不是。
  • @wim: vpmulld 是 Intel AVX2 CPU 上的 2 个依赖微指令,10c 延迟。最好使用 pmaddubswpmaddwd 从 8->16->32,乘以 _mm256_set1_epi8(1)_mm256_set1_epi16(1)。相同数量的乘法微指令,但您不需要 srli
  • @PeterCordes:这是水平和的一个很好的解决方案。我不知道这一系列特定的说明。
  • @wim:感谢@PaulR,我想我是从他的一个回答中了解到的。 (但通常psadbw 会更好,如果你想一直到 64 位水平总和,除非你正在做类似How to implement atoi using SIMD? 的事情,你可以按不同的数量缩放不同的元素。)
最近更新 更多