循环分块优化:GCC与Clang性能差异探究
循环分块优化在GCC与Clang下的性能差异原因分析
我尝试使用循环分块(Loop Tiling)方法进行L1缓存优化,实现了原循环与分块循环的代码,并通过基准测试对比两者性能。测试发现:在GCC 12.2 -O3编译下,分块循环性能优于原循环;但在Clang 15.0.0 -O3编译下,分块循环性能与原循环相当甚至更差,添加-march=native参数后,分块循环性能差10倍。以下是测试代码、性能结果及汇编代码,现将差异原因分析如下:
测试代码
#include <chrono> #include <iostream> const int N = 10000; const int blockSize = 16; int array[N]; int sum; // 原循环 void original_loop() { for (int i = 0; i < N; i++) { sum += array[i]; } } // 分块循环 void tiled_loop() { for (int i = 0; i < N; i += blockSize) { for (int j = 0; j < blockSize; j++) { sum += array[i + j]; } } } int main() { // 初始化数组 for (int i = 0; i < N; i++) { array[i] = i; } // 基准测试原循环 auto start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 100000; i++) { sum = 0; original_loop(); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> elapsed = end - start; std::cout << "Original loop: " << elapsed.count() << " seconds" << std::endl; // 基准测试分块循环 start = std::chrono::high_resolution_clock::now(); for (int i = 0; i < 100000; i++) { sum = 0; tiled_loop(); } end = std::chrono::high_resolution_clock::now(); elapsed = end - start; std::cout << "Tiled loop: " << elapsed.count() << " seconds" << std::endl; return 0; }
性能测试结果
当N=1000000、blockSize=16时
**GCC** Original loop: 11.1892 seconds Tiled loop: 9.67448 seconds **Clang** Original loop: 8.52184 seconds Tiled loop: 8.67858 seconds
当N=10000、blockSize=16时
**GCC** Original loop: 0.094786 seconds Tiled loop: 0.0436597 seconds **Clang** Original loop: 0.0416874 seconds Tiled loop: 0.0610718 seconds
添加-march=native参数后Clang测试结果
Original loop: 0.0292406 seconds Tiled loop: 0.173324 seconds
生成的汇编代码
GCC生成的汇编
original_loop(): mov ecx, DWORD PTR sum[rip] mov eax, OFFSET FLAT:array mov edx, OFFSET FLAT:array+40000 pxor xmm0, xmm0 .L2: paddd xmm0, XMMWORD PTR [rax] add rax, 16 cmp rdx, rax jne .L2 movdqa xmm1, xmm0 psrldq xmm1, 8 paddd xmm0, xmm1 movdqa xmm1, xmm0 psrldq xmm1, 4 paddd xmm0, xmm1 movd eax, xmm0 add eax, ecx mov DWORD PTR sum[rip], eax ret tiled_loop(): pxor xmm1, xmm1 mov eax, OFFSET FLAT:array mov edx, OFFSET FLAT:array+40000 movd xmm3, DWORD PTR sum[rip] movdqa xmm2, xmm1 movdqa xmm0, xmm1 .L6: paddd xmm3, XMMWORD PTR [rax] paddd xmm0, XMMWORD PTR [rax+16] add rax, 64 paddd xmm2, XMMWORD PTR [rax-32] paddd xmm1, XMMWORD PTR [rax-16] cmp rdx, rax jne .L6 paddd xmm0, xmm3 paddd xmm0, xmm2 paddd xmm0, xmm1 movdqa xmm1, xmm0 psrldq xmm1, 8 paddd xmm0, xmm1 movdqa xmm1, xmm0 psrldq xmm1, 4 paddd xmm0, xmm1 movd DWORD PTR sum[rip], xmm0 ret sum: .zero 4 array: .zero 40000
Clang生成的汇编
original_loop(): # @original_loop() pxor xmm0, xmm0 mov eax, 12 movd xmm1, dword ptr [rip + sum] # xmm1 = mem[0],zero,zero,zero lea rcx, [rip + array] .LBB0_1: # =>This Inner Loop Header: Depth=1 paddd xmm1, xmmword ptr [rcx + 4*rax - 48] paddd xmm0, xmmword ptr [rcx + 4*rax - 32] paddd xmm1, xmmword ptr [rcx + 4*rax - 16] paddd xmm0, xmmword ptr [rcx + 4*rax] add rax, 16 cmp rax, 10012 jne .LBB0_1 paddd xmm0, xmm1 pshufd xmm1, xmm0, 238 # xmm1 = xmm0[2,3,2,3] paddd xmm1, xmm0 pshufd xmm0, xmm1, 85 # xmm0 = xmm1[1,1,1,1] paddd xmm0, xmm1 movd dword ptr [rip + sum], xmm0 ret tiled_loop(): # @tiled_loop() mov edx, dword ptr [rip + sum] xor eax, eax lea rcx, [rip + array] .LBB1_1: # =>This Inner Loop Header: Depth=1 movdqa xmm0, xmmword ptr [rcx + 4*rax] movdqa xmm1, xmmword ptr [rcx + 4*rax + 16] paddd xmm1, xmmword ptr [rcx + 4*rax + 48] paddd xmm0, xmmword ptr [rcx + 4*rax + 32] paddd xmm0, xmm1 pshufd xmm1, xmm0, 238 # xmm1 = xmm0[2,3,2,3] paddd xmm1, xmm0 pshufd xmm0, xmm1, 85 # xmm0 = xmm1[1,1,1,1] paddd xmm0, xmm1 movd esi, xmm0 add esi, edx cmp rax, 9983 ja .LBB1_3 movdqa xmm0, xmmword ptr [rcx + 4*rax + 64] movdqa xmm1, xmmword ptr [rcx + 4*rax + 80] paddd xmm1, xmmword ptr [rcx + 4*rax + 112] paddd xmm0, xmmword ptr [rcx + 4*rax + 96] paddd xmm0, xmm1 pshufd xmm1, xmm0, 238 # xmm1 = xmm0[2,3,2,3] paddd xmm1, xmm0 pshufd xmm0, xmm1, 85 # xmm0 = xmm1[1,1,1,1] paddd xmm0, xmm1 movd edx, xmm0 add edx, esi add rax, 32 jmp .LBB1_1 .LBB1_3: mov dword ptr [rip + sum], esi ret array: .zero 40000 sum: .long 0
差异原因分析
向量化优化策略不同
- GCC对分块循环做了更高效的SIMD并行优化:在
tiled_loop的汇编中,一次循环迭代处理4个XMMWORD(共64字节,对应16个int),用多个XMM寄存器并行累加,最后统一合并结果,充分利用了SIMD单元的带宽,比原循环的单XMM寄存器累加效率更高。 - Clang的原循环已经做到极致向量化:Clang对
original_loop的优化直接用两个XMM寄存器交替处理4个XMMWORD,循环迭代次数极少;但分块循环的代码结构被Clang拆分成多次小SIMD累加,还额外增加了pshufd、paddd等冗余的结果合并操作,每次块处理都要把SIMD结果转成整数加到sum相关变量,额外开销抵消了分块的潜在收益。
- GCC对分块循环做了更高效的SIMD并行优化:在
-march=native放大了差异- 指定
-march=native后,Clang会启用CPU原生高级指令集(如AVX2),原循环会进一步优化为更宽的向量操作(如YMM寄存器),带宽利用率大幅提升;但分块循环的嵌套结构限制了Clang的优化空间,依然保留了冗余的块内合并逻辑,甚至因指令集升级后小循环的分支预测、流水线停顿问题更明显,导致性能差距被放大10倍。
- 指定
缓存优化的实际收益有限
- 测试用的数组大小为10000个int(共40KB),刚好处于多数CPU的L1数据缓存范围内(32KB-64KB),原循环本身已经是缓存友好的,分块循环的缓存优化收益本就有限。GCC能通过分块进一步提升SIMD并行度,但Clang的分块循环反而引入了额外的循环嵌套和结果合并开销,最终导致性能下降。
内容的提问来源于stack exchange,提问作者merovingian
相关产品推荐
相关产品推荐

