顺序访问double数组时LLC缓存负载计数异常及高缺失率疑问
一、基础疑问
为什么顺序数组访问会存在较高的缓存缺失率?
二、测试场景与问题详情
我编写了一段C代码用于理解perf工具与缓存机制,对double类型数组进行顺序访问,代码如下:
// test.c #include <stdio.h> #include <stdlib.h> #include <emmintrin.h> int main(int argc, char **argv) { size_t n = 10000000; double* arr; if (posix_memalign((void**)&arr, 64, n * sizeof(double)) != 0) { fprintf(stderr, "posix_memalign failed\n"); exit(1); } for (size_t i = 0; i < n; i += 64) { _mm_clflush(&arr[i]); } for (size_t i = 0; i < n; i++) arr[i] = (double)i; double sum = 0; for (size_t i = 0; i < n; i++) sum += arr[i]; printf("Sum: %f\n", sum); free(arr); return 0; }
我采用无优化方式编译代码:
$ gcc -o test test.c
随后使用perf工具统计LLC相关性能计数器:
$ perf stat -e LLC-loads,LLC-loads-misses -- ./test
得到如下结果:
Sum: 49999995000000.000000
Performance counter stats for './test':
94,765 LLC-loads:u
91,979 LLC-loads-misses:u # 97.06% of all LL-cache accesses0.082974254 seconds time elapsed
0.047802000 seconds user
0.034893000 seconds sys
现有两个疑问待解答:
LLC-loads计数远低于预期,理论上该值应接近10000000/8(缓存行大小为64字节),是否存在perf未对程序整个运行时进行采样的可能?- 缓存缺失率过高:预期每次读取缺失会加载包含8个
double元素的缓存行,整体缺失率应接近1/8,且缓存预取机制应进一步降低缺失率,但实际缺失率高达97.06%,这是为什么?
三、环境信息
- GCC版本:
gcc (GCC) 15.2.0 - Perf版本:
perf version 4.18.0-553.84.1.el8_10.x86_64 - CPU型号:Intel(R) Xeon(R) Gold 6430
- L3缓存大小:61440K
问题解答
针对疑问1:LLC-loads计数远低于预期
首先得明确LLC-loads事件的定义:它统计的是需要从LLC(最后一级缓存)加载数据的请求,而非所有缓存行的访问请求。你的代码里有个关键逻辑:调用_mm_clflush刷掉数组缓存后,紧接着执行了写入操作arr[i] = (double)i——这个写入过程会自动把对应缓存行重新加载到L1/L2缓存中。
等到后续求和阶段读取数组时,数据大多还停留在L1/L2缓存,根本不需要去LLC取数据,所以LLC-loads的计数自然远低于你计算的10000000/8(约125万)。另外,你用的LLC-loads:u只统计用户态的LLC请求,内核态访问不会被计入,但这不是主要原因,核心还是低层缓存命中占了绝大多数。
针对疑问2:97%的LLC缺失率
这个高缺失率其实是个“假象”,核心原因是LLC的访问基数太小:
- 你的数组总大小是80MB(1000万×8字节),而L3缓存只有60MB,整个数组无法完全放入L3。写入阶段结束后,部分缓存行已经被挤出L3,等到求和阶段遍历到这些位置时,就需要从LLC甚至内存重新加载——这些请求才会被统计到
LLC-loads里,而这类请求本身就是因为低层缓存没命中才触发的,所以缺失率自然极高。 - 缓存预取的作用被削弱:写入阶段的预取已经把数据加载到低层缓存,但求和阶段遍历到后半段时,前面的缓存行已经因为容量不足被驱逐,预取的缓存行也很快被替换,导致后续读取不得不重新请求LLC/内存。
- 无优化编译的影响:你没加任何优化编译代码,编译器生成的指令没有充分利用CPU的预取机制,预取效率低下,进一步拉高了缺失率。如果加上
-O2优化,编译器会生成更高效的遍历代码,预取机制能更好地发挥作用,缺失率会降到接近预期的水平。
另外,你可以尝试修改代码:把_mm_clflush循环移到写入之后、求和之前——这样能确保求和阶段的读取从LLC/内存开始,此时LLC-loads会更接近预期的125万,缺失率也会降到正常的1/8左右(甚至更低,因为预取会生效)。
内容的提问来源于stack exchange,提问作者user180574

