将16位掩码转换为16字节掩码

有没有办法转换以下代码:

int mask16 = 0b1010101010101010; // int or short, signed or unsigned, it does not matter

__uint128_t mask128 = ((__uint128_t)0x0100010001000100 << 64) | 0x0100010001000100;

所以要特别清楚,比如:

int mask16 = 0b1010101010101010; 
__uint128_t mask128 = intrinsic_bits_to_bytes(mask16);

或直接敷面膜:

int mask16 = 0b1010101010101010; 
__uint128_t v = ((__uint128_t)0x2828282828282828 << 64) | 0x2828282828282828;
__uint128_t w = intrinsic_bits_to_bytes_mask(v, mask16); // w = ((__uint128_t)0x2928292829282928 << 64) | 0x2928292829282928;

回答

位/字节顺序:除非另有说明,否则这些遵循问题,将 的最低uint16_t有效字节放在__uint128_t(little-endian x86 上的最低内存地址)的最低有效字节中。例如,这是位图的 ASCII 转储所需要的,但它与单个 16 位数字的 base-2 表示的位值打印顺序相反。

有效地将值(返回)到 RDX:RAX 整数寄存器的讨论与大多数正常用例无关,因为您只是从向量寄存器存储到内存中,无论是0/1字节整数还是 ASCII '0'/'1'数字(您可以得到在 a 中没有0/1整数的情况下最有效__m128i,更不用说在 an 中了unsigned __int128)。

目录:

  • SSE2 / SSSE3 版本:如果你想要一个 vector 的结果,例如用于存储一个 char 数组,那很好
    (SSE2 NASM 版本,改组为 MSB 优先打印顺序并转换为 ASCII。)
  • BMI2 pdepunsigned __int128如果您要在标量寄存器中使用结果,则适用于带有 BMI2 的 Intel CPU 上的标量。AMD 慢。
  • 带有乘法 bithack 的纯 C++:对于标量非常合理
  • AVX-512:AVX-512 具有作为使用标量位图的一流操作的屏蔽。pdep如果您将结果用作标量的一半,则可能不如 BMI2 ,否则甚至比 SSSE3 好。
  • 32 位整数的AVX2打印顺序(最低地址的 MSB)转储。
  • 另请参阅intel avx2 中是否有与 movemask 指令相反的指令?对于元素大小和掩码宽度的其他变化。(SSE2 和乘法 bithack 改编自该集合中链接的答案。)

使用 SSE2(最好是 SSSE3)

请参阅@aqrit 的如何使用 x86 SIMD 有效地将 8 位位图转换为 0/1 整数数组的答案

使其适应 16 位 -> 16 字节,我们需要一个 shuffle,将掩码的第一个字节复制到向量的前 8 个字节,将第二个掩码字节复制到向量的高 8 个字节。这是可行的一个SSSE3 pshufb,或punpcklbw same,same+ punpcklwd same,same+punpckldq same,same终于重复的事情了两个64位四字。

typedef unsigned __int128  u128;

u128 mask_to_u128_SSSE3(unsigned bitmap)
{
    const __m128i shuffle = _mm_setr_epi32(0,0, 0x01010101, 0x01010101);
    __m128i v = _mm_shuffle_epi8(_mm_cvtsi32_si128(bitmap), shuffle);  // SSSE3 pshufb

    const __m128i bitselect = _mm_setr_epi8(
        1, 1<<1, 1<<2, 1<<3, 1<<4, 1<<5, 1<<6, 1U<<7,
        1, 1<<1, 1<<2, 1<<3, 1<<4, 1<<5, 1<<6, 1U<<7 );
    v = _mm_and_si128(v, bitselect);
    v = _mm_min_epu8(v, _mm_set1_epi8(1));       // non-zero -> 1  :  0 -> 0
    // return v;   // if you want a SIMD vector result

    alignas(16) u128 tmp;
    _mm_store_si128((__m128i*)&tmp, v);
    return tmp;   // optimizes to movq / pextrq (with SSE4)
}

(要获得 0 / 0xFF 而不是 0 / 1,请替换_mm_min_epu8v= _mm_cmpeq_epi8(v, bitselect)如果您想要一串 ASCII '0'/'1'字符,请执行 cmpeq 和_mm_sub_epi8(_mm_set1_epi8('0'), v)。这避免了 set1(1) 向量常量。)

Godbolt包括测试用例。(对于此版本和其他非 AVX-512 版本。)

# clang -O3 for Skylake
mask_to_u128_SSSE3(unsigned int):
        vmovd   xmm0, edi                                  # _mm_cvtsi32_si128
        vpshufb xmm0, xmm0, xmmword ptr [rip + .LCPI2_0] # xmm0 = xmm0[0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1]
        vpand   xmm0, xmm0, xmmword ptr [rip + .LCPI2_1]    # 1<<0, 1<<1, etc.
        vpminub xmm0, xmm0, xmmword ptr [rip + .LCPI2_2]    # set1_epi8(1)

  # done here if you return __m128i v or store the u128 to memory
        vmovq   rax, xmm0
        vpextrq rdx, xmm0, 1
        ret

BMI2 pdep:对英特尔有利,对 AMD 不利

BMI2pdep在拥有它的 Intel CPU 上很快(自 Haswell 以来),但在 AMD 上很慢(超过十几个 uops,高延迟。)

typedef unsigned __int128  u128;
inline u128 assemble_halves(uint64_t lo, uint64_t hi) {
    return ((u128)hi << 64) | lo; }
// could replace this with __m128i using _mm_set_epi64x(hi, lo) to see how that compiles

#ifdef __BMI2__
#include <immintrin.h>
auto mask_to_u128_bmi2(unsigned bitmap) {
    // fast on Intel, slow on AMD
    uint64_t tobytes = 0x0101010101010101ULL;
    uint64_t lo = _pdep_u64(bitmap, tobytes);
    uint64_t hi = _pdep_u64(bitmap>>8, tobytes);
    return assemble_halves(lo, hi);
}

如果您想要标量寄存器(不是一个向量)中的结果,那很好,否则可能更喜欢 SSSE3 方式。

# clang -O3
mask_to_u128_bmi2(unsigned int):
        movabs  rcx, 72340172838076673    # 0x0101010101010101
        pdep    rax, rdi, rcx
        shr     edi, 8
        pdep    rdx, rdi, rcx
        ret
      # returns in RDX:RAX

带有神奇乘法比特黑客的便携式 C++

在 x86-64 上还不错;AMD 自 Zen 以来拥有快速的 64 位乘法,而 Intel 自 Nehalem 以来就拥有该乘法。一些低功耗的 CPU 仍然有缓慢的imul r64, r64

这个版本可能是最佳的__uint128_t结果,至少对于没有 BMI2 的 Intel 和 AMD 的延迟,因为它避免了到 XMM 寄存器的往返。但是对于吞吐量来说,它有很多指令

请参阅@phuclv 关于如何从 8 个布尔值中创建一个字节(反之亦然)的回答?对于乘法的解释,以及相反的方向。从使用的算法,unpack8bools每进行一次8位一半的的mask

//#include <endian.h>     // glibc / BSD
auto mask_to_u128_magic_mul(uint32_t bitmap) {
    //uint64_t MAGIC = htobe64(0x0102040810204080ULL); // For MSB-first printing order in a char array after memcpy.  0x8040201008040201ULL on little-endian.
    uint64_t MAGIC = 0x0102040810204080ULL;    // LSB -> LSB of the u128, regardless of memory order
    uint64_t MASK  = 0x0101010101010101ULL;
    uint64_t lo = ((MAGIC*(uint8_t)bitmap) ) >> 7;
    uint64_t hi = ((MAGIC*(bitmap>>8)) ) >> 7;

    return assemble_halves(lo & MASK, hi & MASK);
}

如果您要使用 存储__uint128_t到内存memcpy,您可能希望通过使用htole64(0x0102040810204080ULL);(来自GNU / BSD <endian.h>)或等效于始终将输入的低位映射到输出的最低字节来控制主机字节序,即到 acharbool数组的第一个元素。或htobe64用于其他订单,例如用于打印。在常量而不是变量数据上使用该函数允许在编译时进行常量传播。

否则,如果您真的想要一个低位与 u16 输入的低位匹配的 128 位整数,则乘数常数与主机字节序无关;没有对更广泛类型的字节访问。

x86-64 的 clang 12.0 -O3:

mask_to_u128_magic_mul(unsigned int):
        movzx   eax, dil
        movabs  rdx, 72624976668147840   # 0x0102040810204080
        imul    rax, rdx
        shr     rax, 7
        shr     edi, 8
        imul    rdx, rdi
        shr     rdx, 7
        movabs  rcx, 72340172838076673   # 0x0101010101010101
        and     rax, rcx
        and     rdx, rcx
        ret

AVX-512

使用 AVX-512BW这很容易;您可以将掩码用于重复0x01常量的零掩码负载。

__m128i bits_to_bytes_avx512bw(unsigned mask16) {
    return _mm_maskz_mov_epi8(mask16, _mm_set1_epi8(1));

//    alignas(16) unsigned __int128 tmp;
//    _mm_store_si128((__m128i*)&u128, v);  // should optimize into vmovq / vpextrq
//    return tmp;
}

或者避免使用内存常量(因为编译器可以set1(-1) 只使用一个vpcmpeqd xmm0,xmm0):做一个零掩码的绝对值-1。可以提升常量设置,与 set1(1) 相同。

__m128i bits_to_bytes_avx512bw_noconst(unsigned mask16) {
    __m128i ones = _mm_set1_epi8(-1);    // extra instruction *off* the critical path
    return _mm_maskz_abs_epi8(mask16, ones);
}

但请注意,如果进一步进行向量操作,结果maskz_mov可能会优化为其他操作。例如 vec += maskz_mov 可以优化为合并掩码添加。但如果没有,则vmovdqu8 xmm{k}{z}, xmm需要一个 ALU 端口,例如vpabsb xmm{k}{z}, xmm,但vpabsb不能在 Skylake/Ice Lake 上的端口 5 上运行。(vpsubb从清零寄存器中屏蔽零将避免可能出现的吞吐量问题,但随后您将设置 2 个寄存器以避免加载常量。在手写 asm 中,如果您愿意,您只需set1(1)使用vpcmpeqd/vpabsb自己实现以避免常量的 4 字节广播负载。)

(带有 gcc 和 clang 的Godbolt 编译器资源管理器。Clang 看穿-O3 -march=skylake-avx512掩码vpabsb并编译它与第一个版本相同,具有内存常量。)

如果您可以使用向量 0 / -1 而不是 0 / 1,那就更好了:使用return _mm_movm_epi8(mask16). 编译为kmovd k0, edi/vpmovm2b xmm0, k0

如果你想要一个像'0'or这样的 ASCII 字符向量'1',你可以使用_mm_mask_blend_epi8(mask, ones, zeroes). (这应该比合并掩码添加到向量中更有效,set1(1)后者需要额外的寄存器副本,也比 sub betweenset1('0')_mm_movm_epi8(mask16)需要 2 条指令:一个将掩码转换为向量,一个单独的 vpsubb .)


AVX2打印顺序为位(最低地址为 MSB),内存顺序为字节,如 ASCII '0' / '1'

使用这种输出格式的[]分隔符和t制表符,来自此 codereview Q&A:

[01000000]      [01000010]      [00001111]      [00000000]

显然,如果您希望所有 16 或 32 个 ASCII 数字都是连续的,那就更容易了,并且不需要对输出进行混洗以分别存储每个 8 字节的块。在这里发布的大部分原因是它以正确的顺序打印了 shuffle 和 mask 常量,并在结果证明这是问题真正想要的之后显示针对 ASCII 输出优化的版本。

使用如何执行 _mm256_movemask_epi8 (VPMOVMSKB) 的逆运算?,基本上是 256 位版本的 SSSE3 代码。

#include <limits.h>
#include <stdint.h>
#include <stdio.h>
#include <immintrin.h>
#include <string.h>

// /sf/ask/1513554871/
void binary_dump_4B_avx2(const void *input)
{
    char buf[CHAR_BIT*4 + 2*4 + 3 + 1 + 1];  // bits, 4x [], 3x t, n, 0
    buf[0] = '[';
    for (int i=9 ; i<sizeof(buf) - 8; i+=11){ // GCC strangely doesn't unroll this loop
        memcpy(&buf[i], "]t[", 4);       // 4-byte store as a single; we overlap the 0 later
    }
    __m256i  v = _mm256_castps_si256(_mm256_broadcast_ss(input));         // aliasing-safe load; use _mm256_set1_epi32 if you know you have an int
    const __m256i shuffle = _mm256_setr_epi64x(0x0000000000000000,        // low byte first, bytes in little-endian memory order
      0x0101010101010101, 0x0202020202020202, 0x0303030303030303);
    v =  _mm256_shuffle_epi8(v, shuffle);

//    __m256i bit_mask = _mm256_set1_epi64x(0x8040201008040201);    // low bits to low bytes
    __m256i bit_mask = _mm256_set1_epi64x(0x0102040810204080);      // MSB to lowest byte; printing order

    v = _mm256_and_si256(v, bit_mask);               // x & mask == mask
//    v = _mm256_cmpeq_epi8(v, _mm256_setzero_si256());       // -1  /  0  bytes
//    v = _mm256_add_epi8(v, _mm256_set1_epi8('1'));          // '0' / '1' bytes

    v = _mm256_cmpeq_epi8(v, bit_mask);              // 0 / -1  bytes
    v = _mm256_sub_epi8(_mm256_set1_epi8('0'), v);   // '0' / '1' bytes
    __m128i lo = _mm256_castsi256_si128(v);
    _mm_storeu_si64(buf+1, lo);
    _mm_storeh_pi((__m64*)&buf[1+8+3], _mm_castsi128_ps(lo));

    // TODO?: shuffle first and last bytes into the high lane initially to allow 16-byte vextracti128 stores, with later stores overlapping to replace garbage.
    __m128i hi = _mm256_extracti128_si256(v, 1);
    _mm_storeu_si64(buf+1+11*2, hi);
    _mm_storeh_pi((__m64*)&buf[1+11*3], _mm_castsi128_ps(hi));
//    buf[32 + 2*4 + 3] = 'n';
//    buf[32 + 2*4 + 3 + 1] = '';
//    fputs
    memcpy(&buf[32 + 2*4 + 2], "]", 2);  // including ''
    puts(buf);                           // appends a newline
     // appending our own newline and using fputs or fwrite is probably more efficient.
}

void binary_dump(const void *input, size_t bytecount) {
}
 // not shown: portable version, see Godbolt, or my or @chux's answer on the codereview question


int main(void)
{
    int t = 1000000;
    binary_dump_4B_avx2(&t);
    binary_dump(&t, sizeof(t));
    t++;
    binary_dump_4B_avx2(&t);
    binary_dump(&t, sizeof(t));
}

<a rel="nofollow noreferrer" href="https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAM1QDsCBlZAQwBtMQBGAFlJvoCqAZ0wAFAB4gA5AAYppAFZdSrZrVAB9LclIj2yAnjqVMtdAGFUrAK4BbWstPoAMnlqYAcnYBGmYiABmAE5SAAdUIUIjWksbe2VwyMM6V3cvW19/YN1MfWTaBgJmYgJYuwdOHLzowuKCVM8fP0CQoSKSsvjKtrqG9MyWgEpdVGtiZA4pAFIAJgC3ZBssAGopgPNWPFtCIQA6BDXsKZkAQVn52kXrFbXzNvx6fcPjs7mFpcxV9fujJ4Cj07nd7XT63LbbejENx/AGvC5XG7fAhQ9Qwl7ouZYKhuT7YAAaohOHgAIhoIOJBqs5uIMQEsTjlgwACoAJQAkh4AOJsgBiAE1yZT8YSSWSKejAacAPRS5YIAgEUJCEAynrIADWqAAbn4qKxUAB3XZoWxSgCO1kwbWiQilM04ADYZs7ODMpQhDQBaIie0K6ki2b0ITCetw64giT2oKie2y2GYAVgdse1mFszCE6s9mFCeAAHJ6taFbNrbJnvC8tag8Ohlt43MUAJ4adB2UIabgAIQ0zC14hmEDQtDayyrNeWACo3KFrARBuiAOydl7LVfLZAIYp16xUKYJzvmAASJxZGk7bKZE%2B4VM7yxmV5vywCj84L73xLWt%2BWMrrO1Iy24cRVn3d9/wCIC93MAh/0g2h/xkFc128Hc907BCE2JL5MLAaZ9xwz9ENXGhiGWCA3AIZY8DWYkgi%2BW8qO%2BPAAC9MGjCBkKoSlPWWPNP0o2ZlwCYlOE4ecl2/WVOXMcxljaYg1GAXJG2WdBUCtWgcIo6xaGIKxWGWAgEDwIRln1VBQglWi11sNNkFCRsIFmB0ONQqiMJg5130gghUNmGZ/24ecAi/NdVx/bhPW8RsCE%2BNoSE%2BDNlmYWS3GAdg%2BINT5U2IVRQgM4NlhkUzmBi4hCNWBcP1OUKtHjJM8FXLUsOWDQ40TB0NBYNolQ0SJ2ogVq6o67xdOYdAuoIXqhDI2gZznIKQtCsLZTYPAM1Sz0hGYKhPn1Ma%2BOsEQWrapNeswAhOA0HM8ACGZKKoZZG1GZZ1VoQ1HuezcdSS2hKPocqhxHWr2oaoQEB3PVQSE46hrO5ErtzB1AIgGRxBkdGMcxrGZH/JbltM96opi5ZsQjaC62iq0/tMwgCHYbMzDW36bJLYhlJILAyuqpbUZkTg%2BYF/mhcF%2BC0ZmGRxcliXpcl0WZACeXFYV5WlYW8qmuo1dBva3rwaoSGEfzCAtX/MGIfYNXJROH812B%2Brf0m9NM2a7XTpEC7DaR8QUfEPMZG4aX%2BZkP2A/F0S%2BLXH99QNB2TKIAmY6Jq1yrth0GvrR2M3VF2To693L


以上是将16位掩码转换为16字节掩码的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>