问题描述
有什么办法可以转换下面的代码:
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
的最低有效字节(最低内存地址小端 x86)。例如,这就是位图的 ASCII 转储所需要的,但它与单个 16 位数字的 base-2 表示的位值打印顺序相反。
关于有效地将值(返回)到 RDX:RAX 整数寄存器的讨论与大多数正常用例无关,因为您只是从向量寄存器存储到内存,无论是 0
/{{1 }} 字节整数或 ASCII 1
/'0'
数字(您可以最有效地获得 '1'
/0
整数在 1
中,更不用说在 __m128i
中)。
目录:
- SSE2 / SSSE3 版本:如果您想要向量中的结果,则很好,例如用于存储字符数组。
(SSE2 NASM version,改组为 MSB 优先打印顺序并转换为 ASCII。) - BMI2
unsigned __int128
:适用于带有 BMI2 的 Intel CPU 上的标量pdep
,如果您打算在标量寄存器中使用结果。在 AMD 上运行缓慢。 - 带有乘法比特技巧的纯 C++:对于标量非常合理
- AVX-512:AVX-512 具有作为使用标量位图的一流操作的屏蔽。如果将结果用作标量一半,则可能不如 BMI2
unsigned __int128
,否则甚至比 SSSE3 好。 - AVX2 打印顺序(最低地址的 MSB) 32 位整数的转储。
- 另请参阅 is there an inverse instruction to the movemask instruction in intel avx2? 以了解元素大小和掩码宽度的其他变化。 (SSE2 和乘法 bithack 改编自该集合中链接的答案。)
使用 SSE2(最好是 SSSE3)
参见@aqrit 的 How to efficiently convert an 8-bit bitmap to array of 0/1 integers with x86 SIMD 答案
使其适应 16 位 -> 16 字节,我们需要一个 shuffle,将掩码的第一个字节复制到向量的前 8 个字节,将第二个掩码字节复制到向量的高 8 个字节。使用一个 SSSE3 pdep
或使用 pshufb
+ punpcklbw same,same
+ punpcklwd same,same
最终复制最多两个 64 位 qwords 是可行的。
punpckldq same,same
(要获得 0 / 0xFF 而不是 0 / 1,请将 typedef unsigned __int128 u128;
u128 mask_to_u128_SSSE3(unsigned bitmap)
{
const __m128i shuffle = _mm_setr_epi32(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,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)
}
替换为 _mm_min_epu8
。如果您想要一串 ASCII v= _mm_cmpeq_epi8(v,bitselect)
/ '0'
字符,执行 cmpeq 和 '1'
。这避免了 set1(1) 向量常数。)
Godbolt 包括测试用例。 (对于此版本和其他非 AVX-512 版本。)
_mm_sub_epi8(_mm_set1_epi8('0'),v)
BMI2 # 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,1]
vpand xmm0,xmmword ptr [rip + .LCPI2_1] # 1<<0,etc.
vpminub 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,1
ret
:英特尔好,AMD 差
BMI2 pdep
在拥有它的 Intel CPU 上速度很快(自 Haswell 以来),但在 AMD 上速度很慢(超过 12 个 uops,高延迟。)
pdep
如果您想要标量寄存器(不是一个向量)中的结果,那很好,否则可能更喜欢 SSSE3 方式。
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);
}
具有神奇乘法比特黑客的便携式 C++
在 x86-64 上还不错; AMD 自 Zen 以来拥有快速的 64 位乘法,而 Intel 自 Nehalem 以来就拥有该乘法。一些低功耗 CPU 的 # clang -O3
mask_to_u128_bmi2(unsigned int):
movabs rcx,72340172838076673 # 0x0101010101010101
pdep rax,rdi,rcx
shr edi,8
pdep rdx,rcx
ret
# returns in RDX:RAX
此版本可能对于 imul r64,r64
结果是最佳的,至少对于没有 BMI2 的 Intel 和 AMD 的延迟而言是这样,因为它避免了到 XMM 寄存器的往返。但是对于吞吐量来说,它有很多指令
有关乘法和相反方向的解释,请参阅 How to create a byte out of 8 bool values (and vice versa)? 上的 @phuclv 回答。对 __uint128_t
的每个 8 位一半使用 unpack8bools
中的算法一次。
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
(来自 GNU / BSD <endian.h>)或等效项来控制主机字节序总是将输入的低位映射到输出的最低字节,即映射到 htole64(0x0102040810204080ULL);
或 char
数组的第一个元素。或 bool
用于其他订单,例如用于打印。在常量而不是变量数据上使用该函数允许在编译时进行常量传播。
否则,如果您确实想要一个低位与 u16 输入的低位匹配的 128 位整数,则乘数常数与主机字节序无关;没有对更广泛类型的字节访问。
clang 12.0 -O3 for x86-64:
htobe64
AVX-512
这很容易使用 AVX-512BW;您可以将掩码用于来自重复 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
常量的零掩码负载。
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;
}
with just a vpcmpeqd xmm0,xmm0
):执行 set1(-1)
的零掩码绝对值。常量setup可以提升,同set1(1)。
-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);
}
的结果可能会优化为其他操作。例如 vec += maskz_mov 可以优化为合并掩码添加。但如果没有,maskz_mov
需要一个像 vmovdqu8 xmm{k}{z},xmm
这样的 ALU 端口,但 vpabsb xmm{k}{z},xmm
不能在 Skylake/Ice Lake 的端口 5 上运行。 (来自清零寄存器的零掩码 vpabsb
将避免可能出现的吞吐量问题,但随后您将设置 2 个寄存器以避免加载常量。在手写 asm 中,您只需实现 { {1}} 自己使用 vpsubb
/ set1(1)
如果您想避免常量的 4 字节广播加载。)
(Godbolt compiler explorer 与 gcc 和 clang vpcmpeqd
。Clang 看穿了掩码 vpabsb
并编译它与第一个版本相同,具有内存常量。)
如果您可以使用向量 0 / -1 而不是 0 / 1,那就更好了:使用 -O3 -march=skylake-avx512
。仅编译为 vpabsb
/ return _mm_movm_epi8(mask16)
如果您需要ASCII 字符向量,例如kmovd k0,edi
或vpmovm2b xmm0,k0
,您可以使用'0'
。 (这应该比合并掩码添加到需要额外寄存器副本的 '1'
向量中更有效,也比需要 2说明:一个把掩码变成一个向量,一个单独的vpsubb。)
AVX2 位打印顺序(最低地址的 MSB),内存顺序的字节,ASCII '0' / '1'
使用 _mm_mask_blend_epi8(mask,ones,zeroes)
分隔符和 set1(1)
制表符这样的输出格式,来自 this codereview Q&A:
set1('0')
显然,如果您希望所有 16 或 32 个 ASCII 数字都是连续的,那会更容易,并且不需要对输出进行混洗以分别存储每个 8 字节的块。在这里发布的主要原因是,它以正确的顺序打印了 shuffle 和 mask 常量,并在结果证明这正是问题真正想要的内容后显示针对 ASCII 输出优化的版本。
使用 How to perform the inverse of _mm256_movemask_epi8 (VPMOVMSKB)?,基本上是 256 位版本的 SSSE3 代码。
_mm_movm_epi8(mask16)
Runnable Godbolt demo 和 []
。
请注意,GCC10.3 及更早版本是愚蠢的,并且复制 AND/CMPEQ 向量常量,一次为字节,一次为 qword。 (在这种情况下,与零进行比较会更好,或者将 OR 与反转掩码一起使用并与全 1 进行比较)。 GCC11.1 使用 \t
修复了该问题,但仍将其加载两次,作为内存操作数而不是一次加载到寄存器中。 Clang 没有这些问题。
有趣的事实:clang [01000000] [01000010] [00001111] [00000000]
设法将它的第二部分变成了 #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
向量之间的 AVX-512 蒙版混合,而不仅仅是 .set .LC1,.LC2
它使用广播加载、-march=icelake-client
字节洗牌,然后使用位掩码测试掩码。
对于掩码中的每一位,您希望将n位置的一位移动到n位置的字节的低位,即位位置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,zeroes);
}
对于使用 gcc 构建,我使用:
g++ -std=c++11 -march=native -O3 src.cpp -pthread
这将构建正常,但如果您的处理器不支持 AVX512,它将在运行时抛出 illegal instruction
时间。