24

内在:

int mask = _mm256_movemask_epi8(__m256i s1)

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

做这个的最好方式是什么?

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

4

5 回答 5

17

我已经在 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;
}
于 2014-02-10T09:24:20.430 回答
10

这是 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)在低 8 字节中包含“0”的控制寄存器来完成,“1 ' 在接下来的 8 个字节中,等等。
  3. VPOR (_mm256_or_si256)用或为每个字节选择适当的位VPAND (_mm256_and_si256)
  4. 用 设置适当字节的 MSB VPCMPEQB (_mm256_cmpeq_epi8)。将每个字节与0xFF. 如果您希望切换掩码的每一位,请VPAND在上一步使用并与零进行比较。

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

于 2014-02-07T10:41:32.337 回答
4

我最初的方法类似于@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 或全为零。这对于执行矢量化位操作也很有用(例如,与另一个矢量进行 &' 以将不需要的字节/字清零)。

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

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

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

其他指令类似。

于 2015-08-27T11:08:48.590 回答
3

我能想到的唯一合理有效的方法是使用 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);
于 2014-02-07T08:19:33.107 回答
3

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

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

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

  • 使用该常量掩码将寄存器中的每个 32 位整数旋转ymm不同的量,使用_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 中的浮点值一样。

于 2014-02-07T13:54:33.950 回答