【问题标题】:How to perform the inverse of _mm256_movemask_epi8 (VPMOVMSKB)?如何执行 _mm256_movemask_epi8 (VPMOVMSKB) 的逆运算?
【发布时间】:2014-03-04 12:54:49
【问题描述】:

内在:

int mask = _mm256_movemask_epi8(__m256i s1)

创建一个掩码,其32 位对应于s1 的每个字节的最高有效位。在使用位操作(例如BMI2)操作掩码后,我想执行_mm256_movemask_epi8 的逆操作,即创建一个__m256i 向量,其中每个字节的最高有效位包含@987654328 的相应位@。

最好的方法是什么?

编辑: 我需要执行相反的操作,因为内在 _mm256_blendv_epi8 只接受 __m256i 类型掩码而不是 uint32_t。因此,在生成的__m256i 掩码中,我可以忽略每个字节的 MSB 以外的位。

【问题讨论】:

  • 使用 AVX512,您可以使用 _mm256_mask_blend_epi8(__mmask32 k, __m256i a, __m256i b) 使用整数作为掩码
  • 另见my answer on a possible duplicate question。使用vpsllvd 变量移位将掩码的不同位放在每个元素的符号位中。这对于 32b 的元素大小非常有用,但不适用于 8b。

标签: c x86 simd avx avx2


【解决方案1】:

我能想到的唯一合理有效的方法是使用 8 位 LUT:进行 4 x 8 位查找,然后将结果加载到向量中,例如

static const uint64_t LUT[256] = { 0x0000000000000000ULL,
                                   ...
                                   0xffffffffffffffffULL };

uint64_t amask[4] __attribute__ ((aligned(32)));

uint32_t mask;
__m256i vmask;

amask[0] = LUT[mask & 0xff];
amask[1] = LUT[(mask >> 8) & 0xff];
amask[2] = LUT[(mask >> 16) & 0xff];
amask[3] = LUT[mask >> 24];
vmask = _mm256_load_si256((__m256i *)amask);

或者,您可以使用寄存器而不是临时数组,看看您的编译器是否可以做一些不涉及通过内存的更高效的事情:

static const uint64_t LUT[256] = { 0x0000000000000000ULL,
                                   ...
                                   0xffffffffffffffffULL };

uint64_t amask0, amask1, amask2, amask3;

uint32_t mask;
__m256i vmask;

amask0 = LUT[mask & 0xff];
amask1 = LUT[(mask >> 8) & 0xff];
amask2 = LUT[(mask >> 16) & 0xff];
amask3 = LUT[mask >> 24];
vmask = _mm256_set_epi64x(amask3, amask2, amask1, amask0);

事后思考:一个有趣的挑战可能是使用例如Haswell BMI 指令执行相当于 8 -> 64 位 LUT 操作,从而摆脱 LUT。看起来您可以为此使用PDEP,例如

const uint64_t pmask = 0x8080808080808080ULL; // bit unpacking mask for PDEP

uint64_t amask0, amask1, amask2, amask3;

uint32_t mask;
__m256i vmask;

amask0 = _pdep_u64(mask, pmask); mask >>= 8;
amask1 = _pdep_u64(mask, pmask); mask >>= 8;
amask2 = _pdep_u64(mask, pmask); mask >>= 8;
amask3 = _pdep_u64(mask, pmask);
vmask = _mm256_set_epi64x(amask3, amask2, amask1, amask0);

【讨论】:

  • 是的,我想尽可能避免使用 LUT,与我正在执行的基于寄存器的操作相比,它们的成本非常高。
【解决方案2】:

以下是 LUT 或 pdep 指令的替代方法,可能更有效:

  1. 将 32 位掩码复制到某些 ymm 寄存器的低字节和同一寄存器的 16..19 字节。您可以使用临时数组和_mm256_load_si256。或者,您可以将 32 位掩码的单个副本移动到某些 ymm 寄存器的低字节,然后使用 VPBROADCASTD (_mm_broadcastd_epi32) 或其他广播/随机播放指令对其进行广播。
  2. 重新排列寄存器的字节,使低 8 字节(每个)包含掩码的低 8 位,接下来的 8 字节 - 下 8 位等。这可以通过 VPSHUFB (_mm256_shuffle_epi8) 来完成,控制寄存器中包含“0”低 8 个字节,接下来的 8 个字节为“1”,等等。
  3. 使用VPOR (_mm256_or_si256)VPAND (_mm256_and_si256) 为每个字节选择适当的位。
  4. VPCMPEQB (_mm256_cmpeq_epi8) 设置适当字节的MSB。将每个字节与0xFF 进行比较。如果您希望切换掩码的每一位,请在上一步中使用 VPAND 并与零进行比较。

这种方法的额外灵活性是,您可以为步骤 #2 选择不同的控制寄存器,为步骤 #3 选择不同的掩码来打乱位掩码的位(例如,您可以将此掩码以相反的顺序复制到 ymm 寄存器)。

【讨论】:

  • 只需使用_mm256_set1_epi32 并让编译器根据需要使用vpbroadcastd ymm, [mem] 进行广播加载。
  • shuffle后,使用VPAND和VPCMPEQB实现bitmap & (1<<bit) == (1<<bit)。你只需要一个向量常数。
  • 如果您想要0/1 而不是0/0xff,请使用_mm256_min_epu8(and_result, _mm256_set1_epi8(1)) 而不是cmpeq 对AND 掩码。与min(0,1) = 0 相比,具有非零字节的元素的最小值为 1。 (这个技巧来自How to efficiently convert an 8-bit bitmap to array of 0/1 integers with x86 SIMD
【解决方案3】:

这是另一个可能适用于 AVX2 的实现,因为您的问题上有该标签(它未经测试,因为我没有 Haswell 机器)。它类似于 Evgeny Kluev 的答案,但可能需要更少的指令。不过,它需要两个常量 __m256i 掩码。如果您在循环中多次执行此操作,那么提前一次设置这些常量的开销可能可以忽略不计。

  • 使用 _mm_broadcastd_epi32() 将 32 位掩码广播到 ymm 寄存器的所有 8 个插槽。

  • 创建一个 __m256i,其中包含 8 个 32 位整数,其值为 [0, 1, 2, 3, 4, 5, 6, 7](从最低有效元素到最高有效元素)。

  • 使用该常量掩码将 ymm 寄存器中的每个 32 位整数旋转不同的量,使用 _mm256_sllv_epi32()

  • 现在,如果我们将 ymm 寄存器视为保存 8 位整数并查看它们的 MSB,那么该寄存器现在保存字节索引的 MSB [7, 15, 23, 31, 6, 14, 22, 30, 5, 13, 21, 29, 4, 12, 20, 28, 3, 11, 19, 27, 2, 10, 18, 26, 1, 9, 17, 25, 0, 8, 16, 24](从最不重要到最重要) -重要元素)。

  • [0x80, 0x80, 0x80, ...] 的常量掩码使用按位与以将 MSB 与每个字节隔离开来。

  • 使用一系列 shuffle 和/或 permutes 使元素按您想要的顺序返回。不幸的是,8 位整数没有任何对任何置换,就像 AVX2 中的浮点值一样。

【讨论】:

    【解决方案4】:

    我已经在 Haswell 机器上实现了上述三种方法。 Evgeny Kluev 的方法是最快的(1.07 秒),其次是 Jason R 的(1.97 秒)和 Paul R 的(2.44 秒)。下面的代码是使用 -march=core-avx2 -O3 优化标志编译的。

    #include <immintrin.h>
    #include <boost/date_time/posix_time/posix_time.hpp>
    
    //t_icc = 1.07 s
    //t_g++ = 1.09 s
    __m256i get_mask3(const uint32_t mask) {
      __m256i vmask(_mm256_set1_epi32(mask));
      const __m256i shuffle(_mm256_setr_epi64x(0x0000000000000000,
          0x0101010101010101, 0x0202020202020202, 0x0303030303030303));
      vmask = _mm256_shuffle_epi8(vmask, shuffle);
      const __m256i bit_mask(_mm256_set1_epi64x(0x7fbfdfeff7fbfdfe));
      vmask = _mm256_or_si256(vmask, bit_mask);
      return _mm256_cmpeq_epi8(vmask, _mm256_set1_epi64x(-1));
    }
    
    //t_icc = 1.97 s
    //t_g++ = 1.97 s
    __m256i get_mask2(const uint32_t mask) {
      __m256i vmask(_mm256_set1_epi32(mask));
      const __m256i shift(_mm256_set_epi32(7, 6, 5, 4, 3, 2, 1, 0));
      vmask = _mm256_sllv_epi32(vmask, shift);
      const __m256i shuffle(_mm256_setr_epi64x(0x0105090d0004080c,
          0x03070b0f02060a0e, 0x0105090d0004080c, 0x03070b0f02060a0e));
      vmask = _mm256_shuffle_epi8(vmask, shuffle);
      const __m256i perm(_mm256_setr_epi64x(0x0000000000000004, 0x0000000100000005,
          0x0000000200000006, 0x0000000300000007));
      return _mm256_permutevar8x32_epi32(vmask, perm);
    }
    
    //t_icc = 2.44 s
    //t_g++ = 2.45 s
    __m256i get_mask1(uint32_t mask) {
      const uint64_t pmask = 0x8080808080808080ULL; // bit unpacking mask for PDEP
      uint64_t amask0, amask1, amask2, amask3; 
      amask0 = _pdep_u64(mask, pmask);
      mask >>= 8;
      amask1 = _pdep_u64(mask, pmask);
      mask >>= 8;
      amask2 = _pdep_u64(mask, pmask);
      mask >>= 8;
      amask3 = _pdep_u64(mask, pmask);
      return _mm256_set_epi64x(amask3, amask2, amask1, amask0);
    }
    
    int main() {
      __m256i mask;
      boost::posix_time::ptime start(
          boost::posix_time::microsec_clock::universal_time()); 
      for(unsigned i(0); i != 1000000000; ++i)
        { 
          mask = _mm256_xor_si256(mask, get_mask3(i));
        }
      boost::posix_time::ptime end(
          boost::posix_time::microsec_clock::universal_time());
      std::cout << "duration:" << (end-start) << 
        " mask:" << _mm256_movemask_epi8(mask) << std::endl;
      return 0;
    }
    

    【讨论】:

    • +1 用于跟进所有三个建议并提供很好的结果摘要!出于兴趣,您使用的是什么编译器?
    • 谢谢!我同时使用了 icc 和 g++。我已经用优化标志更新了时间。
    • FWIW 我在这里用 clang 运行了一些基准测试并得到了类似的结果。
    • clang 结果:get_mask3: 0.9968 ns, get_mask2: 1.7413 ns, get_mask1: (check = 0) 2.291 ns
    【解决方案5】:

    我最初的方法类似于@Jason R,因为这就是“正常”操作的工作方式,但这些操作中的大多数只关心高位——忽略所有其他位。一旦我意识到这一点,_mm*_maskz_broadcast*_epi*(mask,__m128i) 系列函数就最有意义了。您需要启用 -mavx512vl 和 -mavx512bw (gcc)

    根据掩码设置每个字节的最高位的向量:

    /* convert 16 bit mask to __m128i control byte mask */
    _mm_maskz_broadcastb_epi8((__mmask16)mask,_mm_set1_epi32(~0))
    /* convert 32 bit mask to __m256i control byte mask */
    _mm256_maskz_broadcastb_epi8((__mmask32)mask,_mm_set1_epi32(~0))
    /* convert 64 bit mask to __m512i control byte mask */
    _mm512_maskz_broadcastb_epi8((__mmask64)mask,_mm_set1_epi32(~0))
    

    根据掩码设置每个单词的最高位的向量:

    /* convert 8 bit mask to __m128i control word mask */
    _mm_maskz_broadcastw_epi16((__mmask8)mask,_mm_set1_epi32(~0))
    /* convert 16 bit mask to __m256i control word mask */
    _mm256_maskz_broadcastw_epi16((__mmask16)mask,_mm_set1_epi32(~0))
    /* convert 32 bit mask to __m512i control word mask */
    _mm512_maskz_broadcastw_epi16((__mmask32)mask,_mm_set1_epi32(~0))
    

    根据掩码设置每个双字的最高位的向量:

    /* convert 8 bit mask to __m256i control mask */
    _mm256_maskz_broadcastd_epi32((__mmask8)mask,_mm_set1_epi32(~0))
    /* convert 16 bit mask to __m512i control mask */
    _mm512_maskz_broadcastd_epi32((__mmask16)mask,_mm_set1_epi32(~0))
    

    根据掩码设置每个四字的最高位的向量:

    /* convert 8 bit mask to __m512i control mask */
    _mm512_maskz_broadcastq_epi64((__mmask8)mask,_mm_set1_epi32(~0))
    

    这个问题的具体问题是:_mm256_maskz_broadcastb_epi8((__mmask32)mask,_mm_set1_epi32(~0)),但我将其他问题包括在内以供参考/比较。

    请注意,根据掩码(不仅仅是最高位),每个字节/字/...将全为 1 或全为零。这对于进行向量化位操作也很有用(例如,与另一个向量进行 &'ing 以将不需要的字节/字清零)。

    另一个注意事项:每个_mm_set1_epi32(~0) 可以/应该转换为常量(手动或由编译器),因此它应该编译为一个相当快速的操作,尽管它在测试中可能比在现实生活中稍快因为常数可能会保留在寄存器中。然后将这些转换为VPMOVM2{b,w,d,q} instructions

    编辑:如果您的编译器不支持 AVX512,则内联汇编版本应如下所示:

    inline __m256i dmask2epi8(__mmask32 mask){
      __m256i ret;
      __asm("vpmovm2b   %1, %0":"=x"(ret):"k"(mask):);
      return ret;
    }
    

    其他说明类似。

    【讨论】:

    • 如果您想要 0 / -1,请使用 _mm256_movm_epi8(mask),而不是零掩码广播。 -1 以外的值的另一个选项是_mm256_maskz_mov_epi8(mask32, _mm256_set1_epi8(1))。如果不是 vpmovm2b,广播会很有趣,因为 128 位全一的创建(vpcmpeqd same,same 是特殊情况下的 dep-break)比 512 位(vpternlogd z,z,z, 0xff)稍微便宜一些,但广播是随机播放,可以仅在端口 5 上运行。另请参阅 Convert 16 bits mask to 16 bytes mask 的 AVX-512 部分(主要需要 0 / 1,而不是正常的 0 / -1)
    猜你喜欢
    • 2021-06-13
    • 1970-01-01
    • 1970-01-01
    • 2015-01-24
    • 2012-10-03
    • 1970-01-01
    • 2017-08-03
    • 1970-01-01
    相关资源
    最近更新 更多