为何缓存命中率理论更高的第二个C程序在M1芯片上运行性能更差?
问题核心错误点分析
问题背景
给出的两个C语言程序如下:
#include <stdio.h> #include <stdlib.h> typedef unsigned long long u64; int program_1(u64* a, u64* b) { const u64 lim = 50l * 1000l * 1000l; // Reads arrays u64 sum = 0; for (u64 i = 0; i < lim * 100; ++i) { sum += a[i % lim]; sum += b[i % lim]; } printf("%llu\n", sum); return 0; } int program_2(u64* a, u64* b) { const u64 lim = 50l * 1000l * 1000l; // Reads arrays u64 sum = 0; for (u64 i = 0; i < lim * 100; ++i) { sum += a[i % lim]; } for (u64 i = 0; i < lim * 100; ++i) { sum += b[i % lim]; } printf("%llu\n", sum); return 0; }
原有预期认为程序1交替访问数组会导致缓存频繁失效性能更差,但实际测试程序1速度更快,核心是原有缓存认知存在三个关键错误:
- 错误判断了L1缓存的容量对双数组访问的容纳能力
原预期错误认为交替访问a、b数组时,后加载的缓存行会直接挤出先加载的另一数组的缓存行。实际上M1的L1数据缓存为64KB,以64字节缓存行计算可容纳1024个缓存行,同时存放a、b两个数组的数十个缓存行完全没有压力,根本不会出现交替访问就互相淘汰的情况。
实际程序1的缓存命中逻辑和程序2几乎一致:访问a[0]触发miss后会加载连续8个u64(a[0]~a[7])进入缓存,随后访问b[0]触发miss加载b[0]~b[7]进入缓存,接下来7次对a[1]~a[7]、b[1]~b[7]的访问全部命中,和程序2的缓存命中率没有本质差异,不会出现原预期的次次miss的情况。 - 完全忽略了循环本身的执行开销
程序1仅需要执行1次5*10^9次的循环迭代,程序2需要执行2次总计1*10^10次的循环迭代。每次循环都包含i自增、边界判断、跳转这几个固定操作,程序2的循环控制开销直接是程序1的两倍,这部分额外开销远大于两者几乎可以忽略的缓存命中率差异。 - 未考虑M1芯片的专用硬件优化
M1的L1缓存本身带宽极高,同时内置的硬件预取器可以同时识别多个连续访存流,程序1交替访问a、b两个连续数组的模式完全可以被预取器识别,同步预取两个数组的后续缓存行,进一步抹平了两者的缓存性能差异。
此外程序1的单循环结构更利于CPU做指令级并行优化,同一个循环内的两次访存、两次加法可以被CPU乱序调度并行执行,进一步提升了执行效率。
内容的提问来源于stack exchange,提问作者MaiaVictor
相关产品推荐
相关产品推荐

