【问题标题】:Bit vector operation with AVX2 and SSE2AVX2 和 SSE2 的位向量运算
【发布时间】:2020-02-29 19:08:54
【问题描述】:

我是 AVX2 和 SSE2 指令集的新手,我想详细了解如何使用这些指令集来加速位向量运算。

到目前为止,我已经成功地使用它们通过双/浮点操作对代码进行矢量化。

在这个例子中,我有一个 C++ 代码,它在将位向量(使用 unsigned int)中的一个位设置或不设置为特定值之前检查一个条件:

int process_bit_vetcor(unsigned int *bitVector, float *value, const float threshold, const unsigned int dim)
{
       int sum = 0, cond = 0;

       for (unsigned int i = 0; i < dim; i++) {
            unsigned int *word = bitVector + i / 32;
            unsigned int bitValue = ((unsigned int)0x80000000 >> (i & 0x1f));
            cond = (value[i] <= threshold);
            (*word) = (cond) ? (*word) | bitValue : (*word);
            sum += cond;
        }

        return sum;
}

变量sum只返回条件为真的情况数。

我尝试使用 SSE2 和 AVX2 重写此例程,但没有成功... :-(

是否可以使用 AVX2 和 SSE2 重写此类 C++ 代码? 对这种类型的位操作使用矢量化是否值得? 位向量可能包含数千位,所以我希望使用 SSE2 和 AVX2 来加速会很有趣。

提前致谢!

【问题讨论】:

  • 您将需要 _mm_movemask_ps 和标量 |= 在 32 位掩码数据块上。和popcnt。我认为您的位索引是向后的,在每个 unsigned int 中与您阅读 float value[] 的顺序相反,但我认为这是无意的。
  • 您确定bitValue 是指针,而不是unsigned int
  • 这是一个错字,你是对的它不是一个指针,我将修复代码示例。
  • @PeterCorbes 首先感谢您的 cmets!你能给我一个示例代码吗?我仍然对如何将所有部分组合在一起有点困惑......在此先感谢。
  • 你好@chtz,1)不,它不应该是大端,那是无意的。 2)是的,它可以在调用函数之前包含非零值。问候。

标签: c bit-manipulation simd avx2 sse2


【解决方案1】:

如果dim 是 8 的倍数,则以下内容应该有效(要处理余数,请在末尾添加一个普通循环)。微小的 API 更改:

  • 使用long 而不是unsigned int 作为循环索引(这有助于clang 展开循环)
  • 假设 bitvector 是 little-endian(如 cmets 中所建议的)

在循环内部,bitVector 按字节访问。可能值得将 movemask 的 2 或 4 个结果和位或它们一次组合起来(可能取决于目标架构)。

要计算sum,直接从cmp_ps 运算的结果计算8 个部分和。由于无论如何您都需要位掩码,因此可能值得使用popcnt(理想情况下,将 2、4 或 8 个字节组合在一起后——同样,这可能取决于您的目标架构)。

int process_bit_vector(uint32_t *bitVector32, float *value,
                       const float threshold_float, const long dim) {
  __m256i sum = _mm256_setzero_si256();
  __m256 threshold_vector = _mm256_set1_ps(threshold_float);
  uint8_t *bitVector8 = (uint8_t *)bitVector32;

  for (long i = 0; i <= dim-8; i += 8) {
    // compare next 8 values with threshold
    // (use threshold as first operand to allow loading other operand from memory)
    __m256 cmp_mask = _mm256_cmp_ps(threshold_vector, _mm256_loadu_ps(value + i), _CMP_GE_OQ);
    // true values are `-1` when interpreted as integers, subtract those from `sum`
    sum = _mm256_sub_epi32(sum, _mm256_castps_si256(cmp_mask));
    // extract bitmask
    int mask = _mm256_movemask_ps(cmp_mask);
    // bitwise-or current mask with result bit-vector
    *bitVector8++ |= mask;
  }

  // reduce 8 partial sums to a single sum and return
  __m128i sum_reduced = _mm_add_epi32(_mm256_castsi256_si128(sum), _mm256_extracti128_si256(sum,1));
  sum_reduced = _mm_add_epi32(sum_reduced, _mm_srli_si128(sum_reduced, 8));
  sum_reduced = _mm_add_epi32(sum_reduced, _mm_srli_si128(sum_reduced, 4));

  return _mm_cvtsi128_si32(sum_reduced);
}

神栓链接:https://godbolt.org/z/ABwDPe

  • 出于某种原因,GCC 使用 vpsubd ymm2, ymm0, ymm1; vmovdqa ymm0, ymm2; 而不仅仅是 vpsubd ymm0, ymm0, ymm1
  • Clang 无法将loadvcmpps 连接起来(并使用LE 而不是GE 比较)——如果你不关心如何处理NaN,你可以使用_CMP_NLT_US 代替的_CMP_GE_OQ

大端输出的修订版(未经测试):

int process_bit_vector(uint32_t *bitVector32, float *value,
                       const float threshold_float, const long dim) {
  int sum = 0;
  __m256 threshold_vector = _mm256_set1_ps(threshold_float);

  for (long i = 0; i <= dim-32; i += 32) {
    // compare next 4x8 values with threshold
    // (use threshold as first operand to allow loading other operand from memory)
    __m256i cmp_maskA = _mm256_castps_si256(_mm256_cmp_ps(threshold_vector, _mm256_loadu_ps(value + i+ 0), _CMP_GE_OQ));
    __m256i cmp_maskB = _mm256_castps_si256(_mm256_cmp_ps(threshold_vector, _mm256_loadu_ps(value + i+ 8), _CMP_GE_OQ));
    __m256i cmp_maskC = _mm256_castps_si256(_mm256_cmp_ps(threshold_vector, _mm256_loadu_ps(value + i+16), _CMP_GE_OQ));
    __m256i cmp_maskD = _mm256_castps_si256(_mm256_cmp_ps(threshold_vector, _mm256_loadu_ps(value + i+24), _CMP_GE_OQ));

    __m256i cmp_mask = _mm256_packs_epi16(
        _mm256_packs_epi16(cmp_maskA,cmp_maskB), // b7b7b6b6'b5b5b4b4'a7a7a6a6'a5a5a4a4 b3b3b2b2'b1b1b0b0'a3a3a2a2'a1a1a0a0
        _mm256_packs_epi16(cmp_maskC,cmp_maskD)  // d7d7d6d6'd5d5d4d4'c7c7c6c6'c5c5c4c4 d3d3d2d2'd1d1d0d0'c3c3c2c2'c1c1c0c0
    );                                // cmp_mask = d7d6d5d4'c7c6c5c4'b7b6b5b4'a7a6a5a4 d3d2d1d0'c3c2c1c0'b3b2b1b0'a3a2a1a0

    cmp_mask = _mm256_permute4x64_epi64(cmp_mask, 0x8d);
                // cmp_mask = [b7b6b5b4'a7a6a5a4 b3b2b1b0'a3a2a1a0  d7d6d5d4'c7c6c5c4 d3d2d1d0'c3c2c1c0]
    __m256i shuff_idx = _mm256_broadcastsi128_si256(_mm_set_epi64x(0x00010203'08090a0b,0x04050607'0c0d0e0f));
    cmp_mask = _mm256_shuffle_epi8(cmp_mask, shuff_idx);

    // extract bitmask
    uint32_t mask = _mm256_movemask_epi8(cmp_mask);
    sum += _mm_popcnt_u32 (mask);
    // bitwise-or current mask with result bit-vector
    *bitVector32++ |= mask;
  }

  return sum;
}

这个想法是在应用vpmovmskb 之前对字节进行洗牌。对于 32 个输入值,这需要 5 次 shuffle 操作(包括 3 次 vpacksswb),但总和的计算是使用 popcnt 而不是 4 vpsubd 完成的。 vpermq (_mm256_permute4x64_epi64) 可以通过在比较它们之前策略性地将 128 位一半加载到 256 位向量中来避免。另一个想法(因为无论如何您都需要对最终结果进行洗牌)将部分结果混合在一起(这往往需要 p52*p015 在我检查过的架构上,所以可能不值得)。

【讨论】:

  • 您可以考虑将比较结果向量打包成 32 位块,使用 2x vpackssdw + vpacksswb + vpermq 车道交叉修正来馈送 vpmovmskb,或使用标量操作。我非常不喜欢 foofoo_ 在同一个函数中使用;还有很多其他的下划线从内在函数中飞来飞去,阅读代码时很容易错过;我开始写评论是因为我认为你在使用 uint32_t *bitVector 时遇到了错误。但不是,您的 bitVector 不是函数 arg。)
  • 组合最多 32 位的一个小优点是,您可以对 popcnt / 4 个向量添加一次,而 vpaddd 每个向量一次。没什么大不了的,假设向量的长度小于 2^32 * 8 个浮点数,那么动态计算是个好主意。
  • 对 var 名称进行了很好的更改;名称中的 8 对 32 向读者强调了您以不同的方式访问同一事物。
  • @PeterCordes 我正在考虑合并 2 或 4 个 vmovmskps 结果,但无法计算出任何实际增加吞吐量的东西。我没想到2*vpackssdw+vpacksswb+vpermq。我认为这会更好(花费4*p5,用于8个向​​量,但节省7*p0以减少movmsk操作(在这种情况下,popcnt也可能更好)。我不会更新这个答案,随意写一个改进的版本。
  • vcmpps + vmovmskps + vpaddd (每个 1,假设负载的微融合 + vcmpps 这意味着避免索引寻址模式)+ or [mem],reg (2) 已经是每个输入向量/每个 5 uops输出字节没有循环控制的空间,即使在为 icelake 展开时(5 宽问题/重命名)。如果您需要将or 放入内存而不是仅仅存储,我认为合并会稍微提前。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-15
  • 2013-03-17
  • 2013-07-24
  • 2019-10-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多