寄存器到高频访问变量的意外慢性能问题求助
背景与测试代码
我正在通过以下示例学习缓存工作机制:
#include <stdio.h> #include <stdint.h> #include <stdlib.h> typedef uint32_t data_t; const int U = 10000000; // size of the array. 10 million vals ~= 40MB const int N = 100000000; // number of searches to perform int main() { data_t* data = (data_t*) malloc(U * sizeof(data_t)); if (data == NULL) { free(data); printf("Error: not enough memory\n"); exit(-1); } // fill up the array with sequential (sorted) values. int i; for (i = 0; i < U; i++) { data[i] = i; } printf("Allocated array of size %d\n", U); printf("Summing %d random values...\n", N); data_t val = 0; data_t seed = 42; for (i = 0; i < N; i++) { int l = rand_r(&seed) % U; val = (val + data[l]); } free(data); printf("Done. Value = %d\n", val); return 0; }
perf分析结果
使用perf record ./sum和perf report分析慢随机访问循环,得到相关注解:
0.05 │ mov -0x18(%rbp),%eax ▒ 0.07 │ mov -0x10(%rbp),%rcx ▒ │ movslq -0x20(%rbp),%rdx ▒ 0.03 │ add (%rcx,%rdx,4),%eax ▒ 95.39 │ mov %eax,-0x18(%rbp) ▒ 1.34 │ mov -0x14(%rbp),%eax ▒ │ add $0x1,%eax ◆ │ mov %eax,-0x14(%rbp)
各内存位置对应的变量:
-0x18存储val-0x10存储data-0x14存储i-0x20存储l
左侧数字为指令耗时占比。
疑问
我原本认为add (%rcx,%rdx,4),%eax指令耗时最多,因为它需要对data[l]执行随机访问加载——我的L1缓存大小为64KB(即16K个整数),该操作命中L1缓存的概率仅约0.16%,理应较慢。但实际耗时最多的是将寄存器%eax的值存入val的mov %eax,-0x18(%rbp)指令,而val是高频访问变量,肯定在缓存中。请问这是为什么?
原因分析
这是CPU乱序执行与perf采样机制共同作用的结果:
乱序执行的延迟隐藏
CPU会通过乱序执行来掩盖内存访问的延迟。当add (%rcx,%rdx,4),%eax发起慢内存加载时,CPU不会原地等待,而是先调度执行后续的mov %eax,-0x18(%rbp)指令。但这个mov指令实际无法完成,它必须等前面的加载操作完成、add指令执行结束后,才能将正确的值写回内存。perf采样的计数逻辑
perf基于时钟周期采样统计指令耗时占比:当CPU因等待内存加载进入空闲周期时,采样计数器会将这些周期归属给当前处于“等待提交结果”状态的指令。在这里,mov %eax,-0x18(%rbp)就是那个被“背锅”的指令——它一直在等待前面的加载完成,所以大部分内存等待周期都被算到了这个指令头上。缓存未命中才是真正瓶颈
你的判断没错:随机访问data[l]确实是性能瓶颈,它产生的数百个时钟周期延迟,被CPU乱序执行引擎转移到了后续的mov指令上。perf的采样结果只是把延迟的统计“归属”到了后续指令,但程序变慢的核心原因依然是缓存未命中导致的内存加载慢。
可以通过perf stat -e cache-misses ./sum验证:你会看到极高的缓存未命中次数,这直接证明了内存加载延迟是性能问题的根源。
内容的提问来源于stack exchange,提问作者Steven Mai

