将 16 位掩码转换为 16 字节掩码 AVX2 位打印顺序最低地址的 MSB,内存顺序的字节,ASCII '0' / '1'

如何解决将 16 位掩码转换为 16 字节掩码 AVX2 位打印顺序最低地址的 MSB,内存顺序的字节,ASCII '0' / '1'

有什么办法可以转换下面的代码:

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,edivpmovm2b 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 时间。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-
参考1 参考2 解决方案 # 点击安装源 协议选择 http:// 路径填写 mirrors.aliyun.com/centos/8.3.2011/BaseOS/x86_64/os URL类型 软件库URL 其他路径 # 版本 7 mirrors.aliyun.com/centos/7/os/x86
报错1 [root@slave1 data_mocker]# kafka-console-consumer.sh --bootstrap-server slave1:9092 --topic topic_db [2023-12-19 18:31:12,770] WARN [Consumer clie
错误1 # 重写数据 hive (edu)&gt; insert overwrite table dwd_trade_cart_add_inc &gt; select data.id, &gt; data.user_id, &gt; data.course_id, &gt; date_format(
错误1 hive (edu)&gt; insert into huanhuan values(1,&#39;haoge&#39;); Query ID = root_20240110071417_fe1517ad-3607-41f4-bdcf-d00b98ac443e Total jobs = 1
报错1:执行到如下就不执行了,没有显示Successfully registered new MBean. [root@slave1 bin]# /usr/local/software/flume-1.9.0/bin/flume-ng agent -n a1 -c /usr/local/softwa
虚拟及没有启动任何服务器查看jps会显示jps,如果没有显示任何东西 [root@slave2 ~]# jps 9647 Jps 解决方案 # 进入/tmp查看 [root@slave1 dfs]# cd /tmp [root@slave1 tmp]# ll 总用量 48 drwxr-xr-x. 2
报错1 hive&gt; show databases; OK Failed with exception java.io.IOException:java.lang.RuntimeException: Error in configuring object Time taken: 0.474 se
报错1 [root@localhost ~]# vim -bash: vim: 未找到命令 安装vim yum -y install vim* # 查看是否安装成功 [root@hadoop01 hadoop]# rpm -qa |grep vim vim-X11-7.4.629-8.el7_9.x
修改hadoop配置 vi /usr/local/software/hadoop-2.9.2/etc/hadoop/yarn-site.xml # 添加如下 &lt;configuration&gt; &lt;property&gt; &lt;name&gt;yarn.nodemanager.res