簡體   English   中英

將 16 位掩碼轉換為 16 字節掩碼

[英]Convert 16 bits mask to 16 bytes mask

有什么辦法可以轉換以下代碼:

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的 LSB 放在__uint128_t的最低有效字節中(little-endian x86 上的最低 memory 地址)。 例如,對於 bitmap 的 ASCII 轉儲,這就是您想要的,但它與單個 16 位數字的 base-2 表示的位值打印順序相反。

有效地將值(返回)到 RDX:RAX integer 寄存器的討論與大多數正常用例無關,因為您只需從向量寄存器存儲到 memory,無論是0 / 1字節整數還是 ASCII '0' / '1'位(在__m128i中沒有0 / 1整數,更不用說在unsigned __int128 )。

目錄:

  • SSE2 / SSSE3 版本:如果您希望將結果保存在 vector 中,例如用於存儲 char 數組,則很好
    SSE2 NASM 版本,改組為 MSB 優先打印順序並轉換為 ASCII。)
  • BMI2 pdep :適用於帶有 BMI2 的 Intel CPU 上的標量unsigned __int128 ,如果您要在標量寄存器中使用結果。 AMD慢。
  • 純 C++ 與乘法 bithack:標量相當合理
  • AVX-512:AVX-512 使用標量位圖將掩碼作為一級操作。 如果您將結果用作標量一半,則可能不如 BMI2 pdep好,否則甚至比 SSSE3 更好。
  • 32 位 integer 的 AVX2打印順序(MSB 在最低地址)轉儲。
  • 另請參閱intel avx2 中的 movemask 指令是否有逆指令? 對於元素大小和掩碼寬度的其他變化。 (SSE2 和乘法 bithack 改編自該集合鏈接的答案。)

使用 SSE2(最好是 SSSE3)

請參閱@aqrit 的如何使用 x86 SIMD 答案有效地將 8 位 bitmap 轉換為 0/1 整數數組

調整它以使用 16 位 -> 16 字節,我們需要一個 shuffle 將掩碼的第一個字節復制到向量的前 8 個字節,並將第二個掩碼字節復制到高 8 個向量字節。 這可以通過一個 SSSE3 pshufbpunpcklbw same,same + punpcklwd same,same + punpckldq same,same最終復制最多兩個 64 位 qwords 來實現。

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_epu8替換為v= _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 :對 Intel 好,對 AMD 不好

BMI2 pdep在擁有它的 Intel CPU 上速度很快(從 Haswell 開始),但在 AMD 上卻非常慢(超過 12 個微指令,高延遲。)

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++ 帶魔術乘法 bithack

在 x86-64 上還不錯; 自 Zen 以來的 AMD 擁有快速的 64 位乘法,而英特爾自 Nehalem 以來就有。 一些低功耗 CPU 仍然有較慢imul r64, r64

這個版本對於__uint128_t結果可能是最佳的,至少對於沒有 BMI2 的 Intel 和 AMD 的延遲,因為它避免了到 XMM 寄存器的往返。 但是對於吞吐量,它是相當多的指令

請參閱@phuclv 關於如何從 8 個布爾值中創建一個字節的答案(反之亦然)? 有關乘法的解釋,以及相反的方向。 mask的每個 8 位一半使用一次unpack8bools中的算法。

//#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);
}

如果您要使用memcpy__uint128_t存儲到 memory ,您可能需要使用htole64(0x0102040810204080ULL);來控制主機字節序。 (來自GNU / BSD <endian.h> )或等價於 map 的低位輸入到 output 的最低字節,即到charbool數組的第一個元素。 或者htobe64用於其他訂單,例如用於打印。 在常量而不是變量數據上使用 function 允許在編譯時進行常量傳播。

否則,如果您真的想要一個 128 位 integer 的低位與 u16 輸入的低位匹配,則乘數常數與主機字節序無關; 沒有對更廣泛類型的字節訪問。

clang 12.0 -O3 用於 x86-64:

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;
}

或者避免使用 memory 常量(因為編譯器可以只使用vpcmpeqd xmm0,xmm0執行set1(-1) ):執行-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需要像vpabsb xmm{k}{z}, xmm這樣的 ALU 端口,但vpabsb不能在 Skylake/Ice Lake 的端口 5 上運行。 (來自零寄存器的零掩碼vpsubb將避免可能的吞吐量問題,但隨后您將設置 2 個寄存器以避免加載常量。在手寫 asm 中,您只需使用set1(1)實現vpcmpeqd / vpabsb如果您想避免常量的 4 字節廣播負載,請自己使用。)

( Godbolt compiler explorer with gcc and clang -O3 -march=skylake-avx512 . Clang sees through the masked vpabsb and compiles it the same as the first version, with a memory constant.)

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

如果您想要'0''1'之類的 ASCII 字符向量,可以使用_mm_mask_blend_epi8(mask, ones, zeroes) (這應該比將合並掩碼添加到set1(1)的向量中需要額外的寄存器副本更有效,並且也比set1('0')_mm_movm_epi8(mask16)之間需要 2 條指令的 sub 更好:一個將掩碼轉換為向量,以及一個單獨的 vpsubb。)


AVX2 位按打印順序(MSB 在最低地址),字節按內存順序,ASCII '0' / '1'

使用[]分隔符和\t制表符,如 output 格式,來自此 codereview Q&A

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

顯然,如果您希望所有 16 或 32 個 ASCII 數字連續,那會更容易,並且不需要改組 output 以分別存儲每個 8 字節塊。 在這里發布的主要原因是它具有正確的打印順序的隨機播放和掩碼常量,並且在事實證明這是問題真正想要的之后顯示針對 ASCII output 優化的版本。

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

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

// https://stackoverflow.com/questions/21622212/how-to-perform-the-inverse-of-mm256-movemask-epi8-vpmovmskb
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] = '\0';
//    fputs
    memcpy(&buf[32 + 2*4 + 2], "]", 2);  // including '\0'
    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));
}

帶有gcc -O3 -march=haswell的可運行Godbolt 演示

請注意,GCC10.3 和更早版本是啞的,並且復制 AND/CMPEQ 向量常量,一次作為字節,一次作為 qwords。 (在這種情況下,與零比較會更好,或者使用帶有反轉掩碼的 OR 並與全一比較)。 GCC11.1 使用.set.LC1,.LC2 .LC2 修復了該問題,但仍將其加載兩次,因為 memory 操作數而不是將一次加載到寄存器中。 Clang 沒有這些問題。

有趣的事實:clang -march=icelake-client設法將其第二部分轉換為'0''1'向量之間的 AVX-512 掩碼混合,但它不僅僅使用kmov ,它使用廣播負載、 vpermb字節洗牌,然后使用位掩碼測試掩碼。

對於掩碼中的每個位,您希望將 position n處的位移動到 position n處字節的低位,即位 position 8 * n 您可以使用循環執行此操作:

__uint128_t intrinsic_bits_to_bytes(uint16_t mask)
{
    int i;
    __uint128_t result = 0;

    for (i=0; i<16; i++) {
        result |= (__uint128_t )((mask >> i) & 1) << (8 * i);
    }
    return result;
}

如果您可以使用 AVX512,則可以在一條指令中完成,無需循環:

#include <immintrin.h>

__m128i intrinsic_bits_to_bytes(uint16_t mask16) {
    const __m128i zeroes = _mm_setzero_si128();
    const __m128i ones = _mm_set1_epi8(1);;
    return _mm_mask_blend_epi8(mask16, ones, zeroes);
}

對於使用 gcc 進行構建,我使用:

g++ -std=c++11 -march=native -O3 src.cpp -pthread

這樣可以構建,但如果您的處理器不支持 AVX512,它將在運行時拋出illegal instruction

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM