【问题标题】:Fastest way to unpack 32 bits to a 32 byte SIMD vector将 32 位解压缩为 32 字节 SIMD 向量的最快方法
【发布时间】:2014-08-05 05:18:36
【问题描述】:

将 32 位存储在内存中的 uint32_t 中,将每个位解压缩到 AVX 寄存器的单独字节元素的最快方法是什么?这些位可以位于其各自字节内的任何位置。

编辑:澄清一下,我的意思是位 0 到字节 0,位 1 到字节 1。显然,字节内的所有其他位都为零。目前我能做的最好的事情是 2 PSHUFB 并且每个位置都有一个掩码寄存器。

如果uint32_t是位图,那么对应的向量元素应该是0或者非0。 (也就是说,我们可以得到一个带有vpcmpeqb 的向量掩码,以对抗全零向量)。

https://software.intel.com/en-us/forums/topic/283382

【问题讨论】:

  • 您使用什么语言?您是否尝试过一些太慢的方法?
  • C 与英特尔内在函数。我尝试了明显的方法:广播 u32,然后使用变量移位或乘法来移位每个 u32。但它开始变得复杂,需要几个寄存器来进行掩码。然后合并。我想我在几年前看到过类似的东西,在一些视频编解码器或其他东西的组装中。
  • 广播优先。对于 AVX2,然后使用 _mm256_and_si256。使用 AVX,您需要拆分通道,执行 _mm_and_si128 两次,然后加入高低。
  • @alecco,我发布了使用 AVX 执行此操作的答案。使用 AVX2 会更简单一些。
  • AVX512BW:VPMOVM2B ymm1, k1:根据k1中的对应位,将ymm1的每个字节设置为0或-1。如果掩码尚未在掩码寄存器中,则还需要KMOVD k1, k2/m32KMOVD k1, r32。显然,您也可以将 64 位掩码放入 512b zmm 寄存器中。

标签: x86 simd avx bitmask avx2


【解决方案1】:

将 32 位整数 x 的 32 位“广播”到 256 位 YMM 寄存器 z 的 32 字节或两个 128 位 XMM 寄存器 z_lowz_high 的 16 字节您可以执行以下操作。

使用 AVX2:

__m256i y = _mm256_set1_epi32(x);
__m256i z = _mm256_shuffle_epi8(y,mask1);
z = _mm256_and_si256(z,mask2);

如果没有 AVX2,最好使用 SSE:

__m128i y = _mm_set1_epi32(x);      
__m128i z_low  = _mm_shuffle_epi8(y,mask_low);
__m128i z_high = _mm_shuffle_epi8(y,mask_high); 
z_low  = _mm_and_si128(z_low ,mask2);
z_high = _mm_and_si128(z_high,mask2);

掩码和工作示例如下所示。如果您打算多次执行此操作,您可能应该 在主循环之外定义掩码。

#include <immintrin.h>
#include <stdio.h>

int main() {
    int x = 0x87654321;

    static const char mask1a[32] = {
        0x00, 0x00, 0x00, 0x00,
        0x00, 0x00, 0x00, 0x00,
        0x01, 0x01, 0x01, 0x01,
        0x01, 0x01, 0x01, 0x01,
        0x02, 0x02, 0x02, 0x02,
        0x02, 0x02, 0x02, 0x02,
        0x03, 0x03, 0x03, 0x03,
        0x03, 0x03, 0x03, 0x03
    };

    static const char mask2a[32] = {
        0x01, 0x02, 0x04, 0x08,
        0x10, 0x20, 0x40, 0x80,
        0x01, 0x02, 0x04, 0x08,
        0x10, 0x20, 0x40, 0x80,
        0x01, 0x02, 0x04, 0x08,
        0x10, 0x20, 0x40, 0x80,
        0x01, 0x02, 0x04, 0x08,
        0x10, 0x20, 0x40, 0x80,
    };

char out[32];

#if defined ( __AVX2__ )
    __m256i mask2 = _mm256_loadu_si256((__m256i*)mask2a);
    __m256i mask1  = _mm256_loadu_si256((__m256i*)mask1a);

    __m256i y =    _mm256_set1_epi32(x);
    __m256i z =    _mm256_shuffle_epi8(y,mask1);
    z = _mm256_and_si256(z,mask2);

    _mm256_storeu_si256((__m256i*)out,z);

#else
    __m128i mask2 = _mm_loadu_si128((__m128i*)mask2a);
    __m128i mask_low  = _mm_loadu_si128((__m128i*)&mask1a[ 0]);
    __m128i mask_high = _mm_loadu_si128((__m128i*)&mask1a[16]);    

    __m128i y = _mm_set1_epi32(x); 
    __m128i z_low  = _mm_shuffle_epi8(y,mask_low);
    __m128i z_high = _mm_shuffle_epi8(y,mask_high);
    z_low  = _mm_and_si128(z_low,mask2);
    z_high = _mm_and_si128(z_high,mask2);

    _mm_storeu_si128((__m128i*)&out[ 0],z_low);
    _mm_storeu_si128((__m128i*)&out[16],z_high);
#endif
    for(int i=0; i<8; i++) {
        for(int j=0; j<4; j++) {        
            printf("%x ", out[4*i+j]);
        }printf("\n");
    } printf("\n");
}

在每个向量元素中获取 0 或 -1:

它需要一个额外的步骤_mm256_cmpeq_epi8 来对抗全零。任何非零都变成0,零变成-1。如果我们不想要这种反转,请使用andnot 而不是and。它反转它的第一个操作数。

__m256i expand_bits_to_bytes(uint32_t x)
{
    __m256i xbcast = _mm256_set1_epi32(x);    // we only use the low 32bits of each lane, but this is fine with AVX2

    // Each byte gets the source byte containing the corresponding bit
    __m256i shufmask = _mm256_set_epi64x(
        0x0303030303030303, 0x0202020202020202,
        0x0101010101010101, 0x0000000000000000);
    __m256i shuf  = _mm256_shuffle_epi8(xbcast, shufmask);

    __m256i andmask  = _mm256_set1_epi64x(0x8040201008040201);  // every 8 bits -> 8 bytes, pattern repeats.
    __m256i isolated_inverted = _mm256_andnot_si256(shuf, andmask);

    // this is the extra step: compare each byte == 0 to produce 0 or -1
    return _mm256_cmpeq_epi8(isolated_inverted, _mm256_setzero_si256());
     // alternative: compare against the AND mask to get 0 or -1,
     // avoiding the need for a vector zero constant.
}

Godbolt Compiler Explorer 上查看。

有关其他元素大小,另请参阅 is there an inverse instruction to the movemask instruction in intel avx2?

【讨论】:

  • @alecco,我更新了我的答案,展示了如何使用 AVX2 执行此操作。我也测试过。
  • 你是明星!非常感谢。希望我能给你更多的支持。
  • 如果您希望得到的字节为 0 或 -1(因此掩码的每一位都扩展为向量字节的所有位),您需要多一步。洗牌后,使用andn 而不是and(反转y)。然后对全零向量使用_mm256_cmpeq_epi8 再次反转。
  • 另外,我会将 mask2a 写为 _mm256_set1_epi64x(0x80'40'20'10'08'04'02'01)。 (为了便于阅读,C++14 ' 分隔符是完全可选的。)为了方便选择 128 和 256,您可以使用 _mm_set1_epi64x(),然后 AVX2 版本可以使用 _mm256_set_m128i(same,same)。这一切都在编译时优化。
  • a duplicate of this that has the same the strategy。 (还建议使用具有一位 unsetvpcmpeqbset1(0xFF) 的掩码的 OR,但全零向量比全零向量略便宜)不确定我是否应该关闭它。我正在寻找要链接的非 AVX 版本。我想这至少有一个非 AVX2 版本。
猜你喜欢
  • 2018-01-18
  • 2019-01-22
  • 1970-01-01
  • 2021-01-05
  • 1970-01-01
  • 2015-03-03
  • 2010-12-16
  • 2020-05-23
相关资源
最近更新 更多