CPU等内存是最浪费时间的事。硬件预取(Hardware Prefetching)让CPU在需要数据之前就把它从内存抓到缓存,等真正要用的时候,数据已经在缓存里了。
1. 为什么需要预取
缓存miss的代价很高。从DDR4内存读数据要100ns左右,而CPU一个周期只有0.3ns(3GHz)。如果每次都要等内存,CPU99%的时间都在干等。
预取的核心思想是:利用访问的局部性,提前加载可能需要的数据。
- 空间局部性:访问地址A,接下来可能访问A+1、A+2
- 时间局部性:访问地址A,接下来可能再次访问A
如果硬件能预测这些模式,提前发起内存请求,就能隐藏延迟。
2. 两种基本预取方式
2.1 流预取(Stream Prefetching)
检测连续的地址访问模式,预取下一个块。
例子:
访问:0x1000, 0x1040, 0x1080 (每个64字节,连续)
预测:接下来要0x10C0
预取:提前加载0x10C0到缓存
Intel的L2 Stream Prefetcher[205]:
- 检测到2个连续访问就确认一个stream
- 可以超前预取多达20个缓存行
- 每个4KB页维护一个forward和一个backward stream
- 最多同时追踪32个stream
2.2 步长预取(Stride Prefetching)
检测固定步长的访问模式。
例子:
访问:0x1000, 0x1100, 0x1200 (步长256字节)
预测:接下来要0x1300
预取:提前加载0x1300
Intel L1 DCU IP Prefetcher[205]:
- 追踪单个load指令的地址模式
- 检测步长最大2KB
- 支持正向和负向步长
3. 流缓冲区(Stream Buffer)
Jouppi在1990年提出流缓冲区[1](#1),Palacharla和Kessler在1994年评估了它作为L2缓存替代方案的效果[2](#2)[3](#3)。
3.1 基本结构
流缓冲区是一个FIFO队列,每个entry保存一个缓存行:
Stream Buffer: [行0] [行1] [行2] [行3] ... [行N]
↑
头部(下一个预取)
当访问命中流缓冲区头部时:
- 数据返回给CPU
- 头部弹出
- 发起新的预取,填充尾部
3.2 与缓存的关系
Palacharla & Kessler的研究[2](#2)比较了两种配置:
- 流缓冲区+L1:流缓冲区作为L2的替代
- 传统L2:更大的统一缓存
发现:对于流式访问,流缓冲区+小L1的性能可以接近大L2,但成本更低。
4. Intel Core i7的预取实现
Intel Core i7有4个硬件预取器[205],可以通过MSR 0x1A4控制:
| 预取器 | 位置 | 功能 | 控制位 |
|---|---|---|---|
| L2 Hardware Prefetcher | L2 | Stream预取,追踪32个stream | Bit 0 |
| L2 Adjacent Cache Line | L2 | 预取相邻128字节对齐块 | Bit 1 |
| L1 DCU Hardware Prefetcher | L1 | Stream预取,基于最近加载 | Bit 2 |
| L1 DCU IP Prefetcher | L1 | Stride预取,追踪load指令 | Bit 3 |
4.1 L2 Stream Prefetcher细节
- 监控L1的读请求,检测升序/降序序列
- 确认stream后,可以超前20行预取
- 动态调整预取深度:
- 负载轻时,预取更远
- 负载重时,只预取到LLC,减少L2污染
4.2 Adjacent Cache Line Prefetcher
把64字节缓存行补全为128字节对齐块:
- 访问0x1000-0x103F,同时预取0x1040-0x107F
- 利用空间局部性,减少后续miss
5. 预取的效果与陷阱
5.1 性能提升
预取对规则访问模式效果显著:
| 程序类型 | 典型加速比 | 原因 |
|---|---|---|
| 科学计算(矩阵运算) | 1.5-2.0x | 规则步长访问 |
| 数据库/图遍历 | 1.0-1.2x | 指针跳跃,难预测 |
| 流式多媒体 | 1.3-1.8x | 顺序访问 |
5.2 预取的代价
带宽浪费:
- 预取的数据如果没被使用,就浪费了内存带宽
- 在带宽受限的系统(如多核共享内存)中,这会伤害其他核心
缓存污染:
- 无用预取会占用缓存空间,踢出真正需要的数据
- 在LRU策略下,预取数据常被当作最近使用,反而把热点数据踢出去
功耗增加:
- 每次预取都要激活DRAM和缓存电路
- 无效预取增加动态功耗
5.3 何时禁用预取
某些场景预取反而有害:
- 随机访问:哈希表、树遍历,模式不可预测
- 带宽受限:内存带宽已满,预取加剧拥塞
- 实时系统:预取引入不确定性
可以通过MSR禁用:
bash
sudo wrmsr -a 0x1a4 0xf # 禁用所有4个预取器
6. 软件预取指令
硬件预取不够智能时,可以用软件预取:
c
// GCC/Clang
__builtin_prefetch(&array[i+16], 0, 1);
// 参数:地址,只读(0)/读写(1),时间局部性高(3)/低(0)
// x86汇编
PREFETCHT0 [addr] // 预取到L1
PREFETCHT1 [addr] // 预取到L2
PREFETCHT2 [addr] // 预取到L3
PREFETCHNTA [addr] // 非临时预取,避免缓存污染
6.1 使用场景
链表遍历:
c
while (node) {
__builtin_prefetch(node->next, 0, 0); // 预取下一个节点
process(node);
node = node->next;
}
分块矩阵乘法:
c
for (int i = 0; i < N; i++) {
__builtin_prefetch(&A[i+4][0], 0, 0); // 提前4行
for (int j = 0; j < N; j++) {
// 计算
}
}
7. 现代预取研究进展
7.1 机器学习预取
传统预取器对复杂模式(如指针追逐)效果有限。近年研究用机器学习预测:
- Delta-LSTM:基于历史访问序列预测下一个地址
- Transformer预取器: attention机制捕捉长距离依赖
这些在数据中心 workload 中效果显著,但硬件实现复杂,尚未广泛商用。
7.2 反馈导向预取
Srinath等人提出Feedback Directed Prefetching[4](#4)[5](#5):
- 监控预取准确率
- 准确率低时降低预取强度
- 准确率高时增加预取深度
这种自适应方法能减少带宽浪费。
8. 总结
硬件预取是提升内存性能的关键技术:
| 预取类型 | 适用场景 | 效果 | 风险 |
|---|---|---|---|
| Stream | 顺序访问 | 高 | 带宽浪费 |
| Stride | 固定步长 | 高 | 缓存污染 |
| 软件预取 | 不规则访问 | 中 | 代码复杂 |
| 机器学习 | 复杂模式 | 高(研究中) | 硬件成本 |
关键认知:
- 预取对规则访问(数组、矩阵)效果最好
- 不规则访问(链表、树)预取效果差,甚至可能有害
- 现代CPU有多种预取器,可以通过MSR调优
- 软件预取作为硬件预取的补充,处理特殊场景
理解预取,写高性能代码时就能知道什么时候依赖硬件,什么时候手动干预。
参考
-
CSDN博客. 缓存预取技术综述(指令和数据). 流缓冲区原理。 ↩︎
-
Palacharla, S., & Kessler, R. E. (1994). Evaluating stream buffers as a secondary cache replacement. ISCA 1994. ↩︎ ↩︎
-
Palacharla & Kessler. ISCA 1994. Stream buffer evaluation. ↩︎
-
Diagnosis and optimization of application prefetching performance. ICS 2013. ↩︎
-
Timing local streams. ICS 2010. ↩︎