Clang O0编译下SSE内联函数为何产生XMM与栈的往返移动指令
问题描述
在代码中将128位变量初始化为零,但无法理解对应汇编代码生成两条冗余移动指令的原因,相关代码如下:
__m128i zeros = reinterpret_cast<__m128i>(_mm_setzero_pd());
对应汇编片段中,xmm0寄存器与0x40(%rsp)栈地址之间存在无意义的往返移动:
00709658: 0F 57 C0 xorps %xmm0, %xmm0 0070965B: 66 0F 29 44 24 40 movapd %xmm0, 0x40(%rsp) 00709661: 66 0F 28 44 24 40 movapd 0x40(%rsp), %xmm0
复现环境为Clang 10.0,未开启编译优化,最小复现代码如下:
template <int LEN> bool SSEEncodeChunk(const char** srcp, char** dstp) { __m128i data = _mm_loadu_si128(reinterpret_cast<const __m128i*>(*srcp)); __m128i zeros = reinterpret_cast<__m128i>(_mm_setzero_pd()); __m128i zero_bytes = _mm_cmpeq_epi8(data, zeros); bool all_zeros = _mm_testz_si128(zero_bytes, zero_bytes); if ((!all_zeros)) { return false; } _mm_storeu_si128(reinterpret_cast<__m128i*>(*dstp), data); *dstp += LEN; *srcp += LEN; return true; }
补充信息
- 6月28日更新:所用Clang为Ubuntu 20.04源自带的
clang version 10.0.0-4ubuntu1 -O0编译选项下(已确认开启-fomit-frame-pointer)的完整汇编中存在大量xmm寄存器与栈偏移地址的往返移动,最初提问仅截取了部分片段,完整反汇编如下:
(lldb) disassemble --name SSEEncodeChunk index_type_traits_test`SSEEncodeChunk<16>: [0x709630] <+0>: subq $0x58, %rsp [0x709634] <+4>: movq %rdi, -0x38(%rsp) [0x709639] <+9>: movq %rsi, -0x40(%rsp) [0x70963e] <+14>: movq -0x38(%rsp), %rax [0x709643] <+19>: movq (%rax), %rax [0x709646] <+22>: movq %rax, -0x28(%rsp) [0x70964b] <+27>: movq -0x28(%rsp), %rax [0x709650] <+32>: movups (%rax), %xmm0 [0x709653] <+35>: movaps %xmm0, -0x50(%rsp) [0x709658] <+40>: xorps %xmm0, %xmm0 [0x70965b] <+43>: movapd %xmm0, 0x40(%rsp) ; 最初提问关注的指令位置 [0x709661] <+49>: movapd 0x40(%rsp), %xmm0 [0x709667] <+55>: movapd %xmm0, -0x60(%rsp) [0x70966d] <+61>: movaps -0x50(%rsp), %xmm0 [0x709672] <+66>: movaps -0x60(%rsp), %xmm1 [0x709677] <+71>: movaps %xmm0, 0x30(%rsp) [0x70967c] <+76>: movaps %xmm1, 0x20(%rsp) [0x709681] <+81>: movaps 0x30(%rsp), %xmm0 [0x709686] <+86>: movaps 0x20(%rsp), %xmm1 [0x70968b] <+91>: pcmpeqb %xmm1, %xmm0 [0x70968f] <+95>: movdqa %xmm0, -0x70(%rsp) [0x709695] <+101>: movdqa -0x70(%rsp), %xmm0 [0x70969b] <+107>: movdqa -0x70(%rsp), %xmm1 [0x7096a1] <+113>: movdqa %xmm0, 0x10(%rsp) [0x7096a7] <+119>: movdqa %xmm1, (%rsp) [0x7096ac] <+124>: movdqa 0x10(%rsp), %xmm0 [0x7096b2] <+130>: movdqa (%rsp), %xmm1 [0x7096b7] <+135>: ptest %xmm1, %xmm0 [0x7096bc] <+140>: sete %cl [0x7096bf] <+143>: movzbl %cl, %edx [0x7096c2] <+146>: cmpl $0x0, %edx [0x7096c5] <+149>: setne %cl [0x7096c8] <+152>: andb $0x1, %cl [0x7096cb] <+155>: movb %cl, -0x71(%rsp) [0x7096cf] <+159>: movb -0x71(%rsp), %cl [0x7096d3] <+163>: xorb $-0x1, %cl [0x7096d6] <+166>: testb $0x1, %cl [0x7096d9] <+169>: jne 0x7096e4 [0x7096df] <+175>: jmp 0x7096ee [0x7096e4] <+180>: movb $0x0, -0x29(%rsp) [0x7096e9] <+185>: jmp 0x70973f [0x7096ee] <+190>: movq -0x40(%rsp), %rax [0x7096f3] <+195>: movq (%rax), %rax [0x7096f6] <+198>: movdqa -0x50(%rsp), %xmm0 [0x7096fc] <+204>: movq %rax, -0x8(%rsp) [0x709701] <+209>: movdqa %xmm0, -0x20(%rsp) [0x709707] <+215>: movdqa -0x20(%rsp), %xmm0 [0x70970d] <+221>: movq -0x8(%rsp), %rax [0x709712] <+226>: movdqu %xmm0, (%rax) [0x709716] <+230>: movq -0x40(%rsp), %rax [0x70971b] <+235>: movq (%rax), %rcx [0x70971e] <+238>: addq $0x10, %rcx [0x709725] <+245>: movq %rcx, (%rax) [0x709728] <+248>: movq -0x38(%rsp), %rax [0x70972d] <+253>: movq (%rax), %rcx [0x709730] <+256>: addq $0x10, %rcx [0x709737] <+263>: movq %rcx, (%rax) [0x70973a] <+266>: movb $0x1, -0x29(%rsp) [0x70973f] <+271>: movb -0x29(%rsp), %al [0x709743] <+275>: andb $0x1, %al [0x709745] <+277>: movzbl %al, %eax [0x709748] <+280>: addq $0x58, %rsp [0x70974c] <+284>: retq
- 开启
-O2优化后,所有冗余往返移动指令完全消失,对应汇编片段如下:
0051E8D5: F3 0F 6F 08 movdqu (%rax), %xmm1 0051E8D9: 66 0F EF C0 pxor %xmm0, %xmm0 0051E8DD: 66 0F 6F D1 movdqa %xmm1, %xmm2 0051E8E1: 66 0F 74 D0 pcmpeqb %xmm0, %xmm2 0051E8E5: 66 0F 38 17 D2 ptest %xmm2, %xmm2 0051E8EA: 0F 85 97 03 00 00 jne 0x51ec87 0051E8F0: F3 0F 7F 0C 3B movdqu %xmm1, (%rbx,%rdi) ... 0051E967: 48 01 F8 addq %rdi, %rax 0051E96A: 48 83 C0 10 addq $0x10, %rax 0051E96E: 48 01 FB addq %rdi, %rbx 0051E971: 48 83 C3 10 addq $0x10, %rbx
原因解答
这是-O0(无优化)模式下Clang的标准代码生成行为,不是编译器bug,和SSE intrinsic的写法也没有关系:
- 无优化模式的核心编译逻辑:Clang在
-O0级别下默认关闭几乎所有指令合并、冗余消除优化,严格遵循一套为调试服务的代码生成规则:每个C/C++变量、每个临时表达式的值计算完成后,必须先写回为它分配的栈内存槽位;后续需要使用这个值时,必须再从对应的栈地址把值读回寄存器。这套逻辑的目的是保证调试体验:在断点处修改任意变量的内存值后,程序会直接用修改后的值继续运行,不会因为变量一直驻留在寄存器里,导致内存修改不生效、调试行为和预期不符。 - 你关注的两条往返移动完全是这套规则的产物:
xorps %xmm0, %xmm0是计算_mm_setzero_pd()的返回值,计算结果暂时存在xmm0寄存器- 第一条
movapd %xmm0, 0x40(%rsp)是把_mm_setzero_pd()生成的临时返回值,写回这个临时值对应的栈槽位 - 第二条
movapd 0x40(%rsp), %xmm0是把临时值从栈上读回寄存器,用来给zeros变量赋值,后面紧跟的movapd %xmm0, -0x60(%rsp)就是把值写到zeros变量自己的栈槽位里。
- 完整汇编里所有类似的XMM寄存器和栈之间的往返搬运,全是这套规则导致的:每一步表达式计算、每一次变量赋值,都会被拆成「计算值到寄存器→寄存器值写栈→后续用值时从栈读回寄存器」的固定流程,编译器完全不会判断这些操作是不是冗余、能不能合并。
- 开
-O2后这些指令全部消失是正常优化结果:优化器会直接把常用变量长期放在寄存器中,跳过所有没必要的栈读写,同时消除冗余临时变量、重复加载操作,最终生成你看到的紧凑高效代码。
补充说明:你用
reinterpret_cast把__m128d类型的_mm_setzero_pd()返回值转成__m128i是完全安全的,全零值在不同类型的128位寄存器里表示完全一致,就算开优化也不会生成额外指令,不需要为了规避这个现象修改这段写法。
内容的提问来源于stack exchange,提问作者DoodleNoodle
相关产品推荐
相关产品推荐

