4、缓存行与预取:缓存行大小(64字节)、硬件预取机制、软件预取指令(prefetch)

聊到内存带宽,有个东西你绕不开——缓存行。

说白了,CPU 和内存之间不是按字节传数据的。它一次最少搬 64 字节。这 64 字节,就是一条缓存行(Cache Line)。

我刚开始做性能优化时,总觉得这玩意儿就是个硬件细节,跟我写代码没关系。直到有一次,我在一个数据链遍历的场景里,死活跑不满内存带宽。CPU 占用率不高,内存控制器也没到极限,但吞吐量就是上不去。后来一查,发现是缓存行在作祟。

嗯,从那以后,我再也不敢小看这 64 字节了。

4.1 缓存行大小:为什么是 64 字节?

先说说为什么是 64 字节,不是 32,也不是 128。

这其实是个权衡。缓存行太小,预取效率低,CPU 得频繁去内存搬数据。缓存行太大,又容易把不需要的数据也搬进来,浪费带宽,还挤占缓存空间。

64 字节,是业界多年实践下来的一个平衡点。x86 和 ARM 的主流处理器,基本都采用这个大小。

核心要点: 你访问一个 4 字节的 int,CPU 实际上把包含这个 int 的 64 字节都搬进了缓存。这意味着,如果你能利用好这 64 字节里的其他数据,你就赚了。否则,你就浪费了宝贵的带宽。

举个例子:

// 场景 A:按顺序访问数组
int arr[1024];
for (int i = 0; i < 1024; i++) {
    process(arr[i]);  // 每次访问,CPU 都搬来 64 字节,后续 15 个 int 直接命中缓存
}

// 场景 B:跳跃访问数组
for (int i = 0; i < 1024; i += 16) {
    process(arr[i]);  // 每次访问,只用了 4 字节,剩下 60 字节全浪费了
}

场景 A 的带宽利用率,可能比场景 B 高十几倍。我在项目中遇到过不少类似场景,很多人写代码时根本没意识到这个差异。

4.2 硬件预取机制:CPU 在帮你干活

光靠缓存行还不够。CPU 还有个更聪明的机制——硬件预取(Hardware Prefetch)。

你想想看,CPU 执行指令的速度比内存快两个数量级。如果每次等数据到了才干活,那大部分时间都在空转。所以硬件预取器会偷偷观察你的内存访问模式,提前把数据拉到缓存里。

常见的硬件预取策略有几种:

预取类型 工作原理 适用场景
顺序预取 检测到连续地址访问,提前加载后续缓存行 数组遍历、流式处理
步长预取 检测到固定间隔的访问模式,预取对应地址 矩阵按列访问、结构体数组
空间预取 访问一个缓存行时,同时加载相邻行 链表遍历(部分场景)

我个人习惯,在写高性能代码前,先想想硬件预取器能不能识别我的访问模式。如果能,那就让它干活;如果不能,我就得手动干预了。

注意: 硬件预取不是万能的。它最怕两种模式:一是随机访问,二是间接跳转(比如通过指针链表)。这两种场景下,预取器基本失效,性能会断崖式下跌。

4.3 软件预取指令:手动控制

当硬件预取搞不定时,就该我们上场了。

x86 提供了 PREFETCH 指令,ARM 有 PRFM。它们的作用是告诉 CPU:「嘿,我马上要用这块数据了,你先帮我搬过来。」

在 C/C++ 里,通常通过编译器内置函数来调用:

// x86 平台
#include <xmmintrin.h>
_mm_prefetch((const char*)addr, _MM_HINT_T0);  // 预取到 L1 缓存

// ARM 平台
__builtin_prefetch(addr, 0, 3);  // 0 表示读,3 表示高局部性

我建议,使用预取指令时,要把握好提前量。预取太早,数据可能被踢出缓存;预取太晚,CPU 还得等。

一个经验值:提前 7-10 个缓存行。比如你正在处理第 i 个元素,可以预取第 i+8 个元素。

void process_with_prefetch(int* data, int n) {
    for (int i = 0; i < n; i++) {
        // 预取 8 个元素之后的数据
        _mm_prefetch((const char*)&data[i + 8], _MM_HINT_T0);
        
        // 处理当前元素
        process(data[i]);
    }
}

小技巧: 我曾经在一个图像处理管线里,用预取指令把带宽利用率从 40% 提到了 85%。关键就是找到那个「提前量」——太早没用,太晚也没用。我一般通过性能计数器(比如 Intel 的 MEM_LOAD_RETIRED 事件)来调优这个值。

4.4 知识体系:缓存行与预取的核心逻辑

下面这张图,帮你理清本章的核心脉络:

缓存行与预取核心逻辑 CPU 访问内存 缓存行:一次搬 64 字节 硬件预取(自动) 软件预取(手动) 顺序预取 · 步长预取 · 空间预取 PREFETCH / PRFM 指令 目标:减少 Cache Miss,提升带宽利用率

4.5 避坑指南

最后,分享几个我踩过的坑:

  • 伪共享(False Sharing): 多线程场景下,两个线程各自修改不同变量,但这两个变量恰好在同一条缓存行里。结果就是,每次修改都会导致缓存行在核间来回同步,性能暴跌。我曾经在一个多线程日志系统里遇到过,加了 padding 对齐到 64 字节后,性能提升了 3 倍。
  • 预取过度: 软件预取不是越多越好。预取指令本身也有开销,而且会占用内存带宽。我建议只在热点路径上使用,并且用性能计数器验证效果。
  • 忽略 NUMA 影响: 在多路服务器上,预取的数据可能来自远端内存。这时候预取反而可能加剧延迟。我一般会结合 numactl 绑定内存节点。

一句话总结: 理解缓存行,用好预取机制,你的内存带宽利用率能翻倍。别让 CPU 闲着等数据,也别让内存总线空跑无用数据。

想要更多资料,联系可可