【问题标题】:AVX2: BitScanReverse or CountLeadingZeros on 8 bit elements in AVX registerAVX2:AVX 寄存器中 8 位元素的 BitScanReverse 或 CountLeadingZeros
【发布时间】:2021-10-29 06:15:07
【问题描述】:

我想提取具有 8 位元素的 256 位 AVX 寄存器中最高设置位的索引。我既找不到bsr 也找不到clz 的实现。

对于具有 32 位元素的 clz,存在带有浮点转换的 bithack,但这对于 8 位可能是不可能的。

目前,我正在研究一个解决方案,我会逐个检查位,稍后会添加,但我想知道是否有更快的方法来做到这一点。

【问题讨论】:

  • 每个8位元素的最高位,还是256位寄存器的最高位?
  • 另外,0 的结果是什么?
  • AVX512可以用吗?
  • @chtz 每个 8 位元素的最高位。 0 不可能是这样,所以无论是最快的。 @AlexGuteniev 最好不要。但如果有使用 AVX512 的解决方案,我很乐意看到它!
  • 我会合并两个基于pshufb 的查找表(用于上半部分和下半部分)。如果没有人更快,我可以稍后再做一个可能的实现。

标签: c++ simd intrinsics avx avx2


【解决方案1】:

这是一个基于vpshufb 的解决方案。这个想法是将输入分成两半,对两者进行查找并组合结果:

__m256i clz_epu8(__m256i values)
{
    // extract upper nibble:
    __m256i hi = _mm256_and_si256(_mm256_srli_epi16(values, 4), _mm256_set1_epi8(0xf));
    // this sets the highest bit for values >= 0x10 and otherwise keeps the lower nibble unmodified:
    __m256i lo = _mm256_adds_epu8(values, _mm256_set1_epi8(0x70));

    // lookup tables for count-leading-zeros (replace this by _mm256_setr_epi8, if this does not get optimized away)
    // ideally, this should compile to vbroadcastf128 ...
    const __m256i lookup_hi = _mm256_broadcastsi128_si256(_mm_setr_epi8(0, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0));
    const __m256i lookup_lo = _mm256_broadcastsi128_si256(_mm_setr_epi8(8, 7, 6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 4));

    // look up each half
    __m256i clz_hi = _mm256_shuffle_epi8(lookup_hi, hi);
    __m256i clz_lo = _mm256_shuffle_epi8(lookup_lo, lo);

    // combine results (addition or xor would work as well)
    return _mm256_or_si256(clz_hi, clz_lo);
}

godbolt-link 粗测:https://godbolt.org/z/MYq74Wxdh

【讨论】:

  • 在我的机器上,没有_mm_broadcastsi128_si256,只有_mm256_broadcastsi128_si256。根据内在指南,两者都存在,但它们的作用相同?!同样由于某种原因,当我使用_mm256_broadcastsi128_si256 运行它时出现内存错误:“accessed RAM at 0xFFFFFFFFFFFFFFFF”,而 Godbolt 不是这种情况。
  • 接受编辑。可能实际上用_mm256_setr_epi8 替换它会更好,这样它至少不会在gcc 上生成次优代码。另一方面,如果你在循环中调用它,编译器应该能够将初始化移出循环。
  • 所以将每一行替换为:__m256i lookup_hi = _mm256_setr_epi8(0, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0); 另外,如果我想要最左边位的位置,即1,我可能必须“反转”这些值,所以对于每个 @ 987654330@,我会设置一个7,一个2一个6等等,对吗?
  • 对于设置为1 的最左边的位,您希望将这些行替换为:__m256i lookup_hi = _mm256_setr_epi8(0, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 0, 4, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7);__m256i lookup_lo = _mm256_setr_epi8(0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3);
  • @simonlet:对我来说看起来不错(我建议用所有可能的值来测试它......)
【解决方案2】:

通常_mm_shuffle_epi8 需要屏蔽以隔离每个半字节以将其用作 LUT,因为设置高位会使输出元素为 0。但是对于 CLZ,如果设置了高位,则正确的结果是整个byte 是0,我们组合的方式意味着lut_lo 可以生成它。

__m128i ssse3_lzcnt_epi8(__m128i v) {
    const __m128i lut_lo = _mm_set_epi8(4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8);
    const __m128i lut_hi = _mm_set_epi8(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 8);
    __m128i t;

    t = _mm_and_si128(_mm_srli_epi16(v, 4), _mm_set1_epi8(0x0F));
    t = _mm_shuffle_epi8(lut_hi, t);
    v = _mm_shuffle_epi8(lut_lo, v);
    v = _mm_min_epu8(v, t);
    return v;
}

与使用 _mm_adds_epu8 并将 LUT 结果与 or 组合相比,这节省了一条指令。

【讨论】:

    【解决方案3】:

    AVX512 解决方案,没试过,但我认为这个想法应该可行:

    // Form four 32-bit vectors with high bytes from the source
    __m256i a0 = _mm256_or_si256(_mm256_slli_si256(a, 3),  _mm256_set1_epi32(0x00FF'FFFF));
    __m256i a1 = _mm256_or_si256(_mm256_slli_si256(a, 2),  _mm256_set1_epi32(0x00FF'FFFF));
    __m256i a2 = _mm256_or_si256(_mm256_slli_si256(a, 1),  _mm256_set1_epi32(0x00FF'FFFF));
    __m256i a3 = _mm256_or_si256(                  a,      _mm256_set1_epi32(0x00FF'FFFF));
    // Count lead bits and shift according to bit position
    __m256i c0 =                   _mm256_lzcnt_epi32(a0);
    __m256i c1 = _mm256_slli_si256(_mm256_lzcnt_epi32(a1), 1);
    __m256i c2 = _mm256_slli_si256(_mm256_lzcnt_epi32(a2), 2);
    __m256i c3 = _mm256_slli_si256(_mm256_lzcnt_epi32(a3), 3);
    //Gather the result
    __m256i r  = _mm256_or_si256(_mm256_or_si256(c0,c1),_mm256_or_si256(c2,c3));
    

    不确定是否比逐个检查更快

    【讨论】:

    • chtz 发布了一个 vpshufb 答案,如果需要,可以移植到 512 位向量。它很可能更快(可能在只有 32 位元素有效的 KNL 上除外),尤其是其巧妙的 adds 可以有效地合并结果。
    • @PeterCordes,今天我了解到pshufb 不仅可以用作 shuffle,还可以用作小型 LUT
    【解决方案4】:

    给定一个目标 AVX 寄存器 _a,这是可行的。如果有需要优化的地方,请告诉我(或直接编辑)。

    __m256i _a;
    __m256i _old_mask = _mm256_set1_epi8(-1);
    __m256i _extract_bitmask, _extracted_bit, _mask;
    
    for (int i = 7; i >= 0; i--)
    {
        // bitmask to extract bit from _a at position i
        _extract_bitmask = _mm256_set1_epi8(1 << i);
    
        // the extracted bit
        _extracted_bit = _mm256_and_si256(_a, _extract_bitmask);
        
        // check if bit at position i is set and if was not set before
        _mask = _mm256_cmpeq_epi8(_extract_bitmask, _extracted_bit);
        _mask = _mm256_and_si256(_mask, _old_mask);
        
        // update mask
        _old_mask = _mm256_andnot_si256(_mask, _old_mask);
    
        // update result according to _mask
        _result = _mm256_blendv_epi8(_result, _mm256_set1_epi8(i), _mask);
    }
    

    【讨论】:

    • 整个策略肯定不是最优的;带有 vpshufb 的查找表显然更好,即使您没有想出像 chtz 的答案那样有效地分离/组合高半和低半部分的方法。但是作为一个学习练习,这里的细节有一些低效率:_extract_bitmask = _mm256_set1_epi8(1 &lt;&lt; i); 可能编译效率很低。从循环外的bmask = set1_epi8(1) 开始,然后用bmask = _mm256_add_epi8(bmask, bmask) 向左移动。即从可变移位+广播到单位向量移位的强度降低优化。
    • 另外,为了风格,没有理由在你的 var 名称上使用前导下划线。内在函数已经有足够的下划线。如果您需要区分同一函数中的非向量,请使用v 作为向量的前导字母。此外,仅在循环外声明循环携带变量。在循环中执行__m256i mask = cmp(bmask, bit),这样很明显这个掩码值不会在下一次迭代中使用。此外,通常您希望在读取 C 变量之前对其进行初始化,例如 result,例如在未找到的情况下使用8,除非您故意希望 a=0 保持不变。
    • 另外,我认为您可以使用_mm256_cmpgt_epi8(_mm256_setzero_si256(), a) 来测试a 的每个元素的符号位,无需任何ANDing,只需每次迭代将a 左移1。跨度>
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-11-18
    • 1970-01-01
    • 2020-11-21
    • 1970-01-01
    相关资源
    最近更新 更多