【问题标题】:Is using AVX2 can implement a faster processing of LZCNT on a word array?使用 AVX2 是否可以在字数组上实现更快的 LZCNT 处理?
【发布时间】:2019-10-02 19:47:21
【问题描述】:

我需要用 LZCNT 反向位扫描一个字数组:16 位。

在英特尔最新一代处理器上,LZCNT 的吞吐量是每个时钟执行 1 次。 AMD Ryzen 的吞吐量似乎是 4。

我正在尝试寻找一种使用 AVX2 指令集更快的算法。

我知道 AVX-512 有 VPLZCNTD 用于 32 位元素,所以如果我有 AVX512CD,我可以打开包装并使用它。

仅使用 AVX2 指令集,是否可以比使用 x86 asm LZCNT 指令更快地编写算法?

【问题讨论】:

  • 您需要一个结果数组,每个元素一个吗?或者您是否对一个大型阵列进行一次扫描以找到整个阵列中的最高设置位?如果是后者,可以使用 AVX2 vpcmpeqb 简单地搜索一个非零字节,然后对其进行位扫描。
  • 你需要对结果做什么?存储吗?如果是这样,即使在 Ryzen 上也可以将结果保存在向量中。如果您仅限于每时钟 1 次存储,则每时钟 4 次 lzcnt 和每时钟 2 次加载将无济于事。
  • @user2927848:您可以在解包后将vpshufb用作4位LUT,然后使用pmaxub合并每个字节的高/低半部分的结果。
  • 我需要一个结果数组,每个元素一个。
  • @GuyB 你只是想存储结果,还是对其进行更多操作?你想存储为uint8uint16uint32吗? 0 的结果是什么(或者0 不会作为输入发生)?

标签: x86 simd avx micro-optimization avx2


【解决方案1】:
#include <immintrin.h>

__m256i avx2_lzcnt_epi16(__m256i v) {
    const __m256i lut_lo = _mm256_set_epi8(
        4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 16,
        4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 16
    );
    const __m256i lut_hi = _mm256_set_epi8(
        0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 16,
        0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 3, 16
    );
    const __m256i nibble_mask = _mm256_set1_epi8(0x0F);
    const __m256i byte_offset = _mm256_set1_epi16(0x0008);
    __m256i t;

    t = _mm256_and_si256(nibble_mask, v);
    v = _mm256_and_si256(_mm256_srli_epi16(v, 4), nibble_mask);
    t = _mm256_shuffle_epi8(lut_lo, t);
    v = _mm256_shuffle_epi8(lut_hi, v);
    v = _mm256_min_epu8(v, t);

    t = _mm256_srli_epi16(v, 8);
    v = _mm256_or_si256(v, byte_offset);
    v = _mm256_min_epu8(v, t);

    return v;
}

// 16 - lzcnt_u16(subwords)
__m256i avx2_ms1b_epi16(__m256i v) {
    const __m256i lut_lo = _mm256_set_epi8(
        12, 12, 12, 12, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 9, 0,
        12, 12, 12, 12, 12, 12, 12, 12, 11, 11, 11, 11, 10, 10, 9, 0
    );
    const __m256i lut_hi = _mm256_set_epi8(
        16, 16, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 14, 14, 13, 0,
        16, 16, 16, 16, 16, 16, 16, 16, 15, 15, 15, 15, 14, 14, 13, 0
    );
    const __m256i nibble_mask = _mm256_set1_epi8(0x0F);
    const __m256i adj = _mm256_set1_epi16(0x1F08);
    __m256i t;

    t = _mm256_and_si256(nibble_mask, v);
    v = _mm256_and_si256(_mm256_srli_epi16(v, 4), nibble_mask);
    t = _mm256_shuffle_epi8(lut_lo, t);
    v = _mm256_shuffle_epi8(lut_hi, v);
    v = _mm256_max_epu8(v, t);

    t = _mm256_srli_epi16(v, 8);
    v = _mm256_sub_epi8(v, adj);
    v = _mm256_max_epi8(v, t);

    return v;
}

对于打包到 uint8 中的结果,请使用 _mm256_packs_epi16()。 对于按正确顺序打包的结果,也请使用_mm256_permute4x64_epi64()

来自r/SIMD 的解决方案。 此处的 cmets 中也描述了此解决方案。

【讨论】:

  • +1,这正是我在 cmets 关于这个问题的想法。 (所有精细的细节都像vpminub 这样我错了,LUT 值也计算出来了,这比我想象的要远。)每个输入向量总共有 9 个向量 ALU 指令,这应该运行在大约在 Haswell/Skylake 上每 3 个周期一个 16x 16 位结果向量。 (有一些前端带宽可用于加载/存储/循环开销。)vpackswb + vpermq 可能会成为 shuffle 吞吐量的瓶颈,但仍比标量 16 位 lzcnt 好得多。也是 Ryzen 的胜利。
  • 简直太棒了!
猜你喜欢
  • 2022-01-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-02-12
  • 2019-03-27
  • 2021-11-22
相关资源
最近更新 更多