为何SIMD+PEXT实现的八进制字符串解析性能不及编译器优化版本?
八进制字符串解析器:编译器优化 vs 自定义SIMD实现性能分析
我为学习SIMD和intrinsics技术,编写了一个满足以下条件的八进制字符串解析器:
- ASCII编码
- 固定长度为12字节
- 非空终止
- 前缀补零
- 保证格式合法
原始非向量化实现
uint64_t parseOctalToBase10(const char message[12]) { uint64_t size = 0; for (int i = 0; i < 12; i++) { uint64_t value = message[i]; value -= '0'; size |= value << (3 * ((12 - 1) - i)); } return size; }
编译命令
使用Clang 19.1.7在Linux上编译:
clang++ -std=c++26 -O3 -mavx512vl -mavx512bw -mbmi2 -S example.cpp -o example.s
生成的汇编代码
_Z18parseOctalToBase10PKc: # @_Z18parseOctalToBase10PKc .cfi_startproc # %bb.0: vpmovsxbq (%rdi), %zmm0 vpsllvq .LCPI0_0(%rip), %zmm0, %zmm0 vpaddq .LCPI0_1(%rip), %zmm0, %zmm0 movsbq 8(%rdi), %rax shlq $9, %rax addq $-24576, %rax # imm = 0xA000 movsbq 9(%rdi), %rcx shlq $6, %rcx addq $-3072, %rcx # imm = 0xF400 movsbq 10(%rdi), %rdx leaq -384(,%rdx,8), %rdx orq %rcx, %rdx orq %rax, %rdx movsbq 11(%rdi), %rcx addq $-48, %rcx orq %rdx, %rcx vextracti64x4 $1, %zmm0, %ymm1 vporq %zmm1, %zmm0, %zmm0 vextracti128 $1, %ymm0, %xmm1 vpor %xmm1, %xmm0, %xmm0 vpshufd $238, %xmm0, %xmm1 # xmm1 = xmm0[2,3,2,3] vpor %xmm1, %xmm0, %xmm0 vmovq %xmm0, %rax orq %rcx, %rax vzeroupper retq
自定义SIMD实现
inline uint64_t parseOctalToBase10SIMD(const char message[12]) { const uint64_t EXTRACTION_MASK_LOW = 0x0707070707070707; const uint64_t EXTRACTION_MASK_HIGH = 0x0000000007070707; __mmask16 loadMask = 0b0000111111111111; __m128i shuffleMask = _mm_setr_epi8(11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 15, 14, 13, 12); __m128i subtrahend = _mm_set1_epi8('0'); __m128i vector = _mm_maskz_loadu_epi8(loadMask, message); vector = _mm_shuffle_epi8(vector, shuffleMask); vector = _mm_sub_epi8(vector, subtrahend); uint64_t low = _mm_cvtsi128_si64(vector); low = _pext_u64(low, EXTRACTION_MASK_LOW); vector = _mm_srli_si128(vector, sizeof(uint64_t)); uint64_t high = _mm_cvtsi128_si64(vector); high = _pext_u64(high, EXTRACTION_MASK_HIGH); return (high << 24) | low; }
生成的汇编代码
_Z22parseOctalToBase10SIMDPKc: # @_Z22parseOctalToBase10SIMDPKc .cfi_startproc # %bb.0: movw $4095, %ax # imm = 0xFFF kmovd %eax, %k1 vmovdqu8 (%rdi), %xmm0 {%k1} {z} vpshufb .LCPI1_0(%rip), %xmm0, %xmm0 # xmm0 = xmm0[11,10,9,8,7,6,5,4,3,2,1,0,15,14,13,12] vpaddb .LCPI1_1(%rip), %xmm0, %xmm0 vmovq %xmm0, %rax movabsq $506381209866536711, %rcx # imm = 0x707070707070707 pextq %rcx, %rax, %rcx vpextrq $1, %xmm0, %rax movl $117901063, %edx # imm = 0x7070707 pextq %rdx, %rax, %rax shlq $24, %rax orq %rcx, %rax retq
性能测试结果
我生成1亿个随机初始化的八进制字符串,循环调用两种实现处理,10次迭代的平均数据如下:
| 实现方式 | 平均完成时长(μs) | σ |
|---|---|---|
| 编译器优化版本 | 131836 | ±2488 |
| 自定义SIMD版本 | 161733 | ±15987 |
编译器优化版本性能明显更优,不仅耗时更短,而且标准差远低于自定义实现。
疑问与讨论
我搞不懂为什么编译器优化的版本更快。查了指令微操作数,没发现我的实现有明显高开销的指令,而且我的指令数更少。
写SIMD版本时,我没法把数据一直留在向量寄存器里,只完成了ASCII转整数的向量操作,之后只能转成64位整数用pextq来位压缩——没找到更好的向量内位堆叠方法。另外我用_mm_maskz_loadu_epi8是因为输入内存不保证对齐,而且长度不是16字节,这会不会是性能瓶颈?但我不想给输入加对齐或长度限制。
自定义实现的高标准差让我怀疑是不是缓存问题,但我不确定。希望能得到相关分析和建议。
内容的提问来源于stack exchange,提问作者User37482946
相关产品推荐
相关产品推荐

