位/字节顺序:除非另有说明,否则这些都跟在问题后面,将 uint16_t 的 LSB 放在 __uint128_t 的最低有效字节(little-endian x86 上的最低内存地址) )。例如,对于位图的 ASCII 转储,这是您想要的,但它与单个 16 位数字的 base-2 表示形式的位值打印顺序相反。
关于有效地将值(返回)到 RDX:RAX 整数寄存器的讨论与大多数正常用例无关,因为您只需从向量寄存器存储到内存中,无论是 0/1 字节整数或 ASCII '0'/'1' 数字(在 __m128i 中没有 0/1 整数,更不用说在 unsigned __int128 中了)。
目录:
使用 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 pshufb 或 punpcklbw 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++
在 x86-64 上还不错;自 Zen 以来的 AMD 拥有快速的 64 位乘法,而英特尔自 Nehalem 以来就有。一些低功耗的 CPU 还是比较慢imul r64, r64
此版本可能最适合__uint128_t 结果,至少对于没有 BMI2 的 Intel 和 AMD 的延迟而言,因为它避免了到 XMM 寄存器的往返。但是对于吞吐量,它是相当多的指令
请参阅@phuclv 在How to create a byte out of 8 bool values (and vice versa)? 上的回答,了解乘法和相反方向的解释。对 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 存储到内存中,您可能希望通过使用htole64(0x0102040810204080ULL);(来自GNU / BSD <endian.h>)或等效于始终映射输入的低位来控制主机字节序到输出的最低字节,即到char 或bool 数组的第一个元素。或htobe64 用于其他订单,例如用于打印。在常量而不是变量数据上使用该函数允许在编译时进行常量传播。
否则,如果你真的想要一个低位匹配 u16 输入的低位的 128 位整数,则乘数常数与主机字节序无关;没有对更广泛类型的字节访问。
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;
}
或者避免使用内存常量(因为编译器可以做set1(-1)with just a 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 需要像 vpabsb xmm{k}{z}, xmm 这样的 ALU 端口,但 vpabsb 不能在 Skylake/Ice Lake 的端口 5 上运行。 (来自零寄存器的零掩码vpsubb 可以避免可能的吞吐量问题,但是您将设置2 个寄存器只是为了避免加载常量。在手写asm 中,您只需实现set1(1)如果您想避免常量的 4 字节广播加载,请自行使用 vpcmpeqd / vpabsb。)
(Godbolt compiler explorer 与 gcc 和 clang -O3 -march=skylake-avx512。Clang 看穿了被屏蔽的 vpabsb 并像第一个版本一样编译它,带有一个内存常量。)
如果您可以使用向量 0 / -1 而不是 0 / 1,那就更好了:使用 return _mm_movm_epi8(mask16)。编译为 kmovd k0, edi / vpmovm2b xmm0, k0
如果您想要ASCII 字符向量,例如'0' 或'1',您可以使用_mm_mask_blend_epi8(mask, ones, zeroes)。 (这应该比合并屏蔽添加到set1(1) 的向量中更有效,这需要额外的寄存器副本,也比set1('0') 和_mm_movm_epi8(mask16) 之间需要两条指令的sub 更好:一个转掩码成一个向量,和一个单独的 vpsubb。)
AVX2 位按 打印 顺序(MSB 在最低地址),字节按内存顺序,ASCII '0' / '1'
使用[] 定界符和\t 制表符这样的输出格式,来自this codereview Q&A:
[01000000] [01000010] [00001111] [00000000]
显然,如果您希望所有 16 位或 32 位 ASCII 数字都是连续的,那会更容易,并且不需要打乱输出以分别存储每个 8 字节块。在这里发帖的主要原因是它具有正确的打印顺序的 shuffle 和 mask 常量,并在结果证明这是问题真正想要的之后显示针对 ASCII 输出优化的版本。
使用How to perform the inverse of _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));
}
Runnable Godbolt demo 与 gcc -O3 -march=haswell。
请注意,GCC10.3 和更早版本是哑的,并且复制 AND/CMPEQ 向量常量,一次作为字节,一次作为 qwords。 (在这种情况下,与零比较会更好,或者使用带有反转掩码的 OR 并与全一比较)。 GCC11.1 使用.set .LC1,.LC2 修复了该问题,但仍将其加载两次,作为内存操作数,而不是将一次加载到寄存器中。 Clang 没有这些问题。
有趣的事实:clang -march=icelake-client 设法将其第二部分转换为 '0' 和 '1' 向量之间的 AVX-512 掩码混合,但不仅仅是 kmov 它使用广播负载,@ 987654410@ 字节洗牌,然后用位掩码测试到掩码。