访存域 RAW:Load→Use 与 Store→Load —— 5 拍起步、200 拍封顶的真依赖
本篇是数据冒险拆分的访存 RAW 篇:把 RAW 里"数据不在流水线内"的两种形态讲透——Load→Use(走 AGU→TLB→Store Buffer→L1 全路径,L1 hit 也至少 5 拍,延迟随命中率浮动到 200+ 拍)与 Store→Load(靠 store buffer 地址比对 forwarding,命中零额外延迟,部分重叠/不对齐则失效)。ALU→ALU 前推(寄存器域 RAW)见 RAW 篇;L1 miss 之后的连锁反应与 MLP 见 miss 连锁反应篇。
一、最典型的 RAW:Load → Use
这才是真实代码里最常见的 RAW 瓶颈。Load µop 的数据不是 ALU 1 拍就能算出来的——它要走完 LSU 的完整流水线:
add eax, [rsi+8] ; 拆成两条 µop
; µop₁: LD tmp, [rsi+8] ← 数据要等 MEM 级才出来
; µop₂: ADD eax, tmp ← 必须等 µop₁和 ALU→ALU 比,Load→Use 的 RAW 有三层不一样:
数据不是 ALU 算出来的。ALU→ALU 只有一个"谁生产、谁消费"的问题,数据在 ALU 里 1 拍就算完。Load 的数据来自内存层次——AGU 算地址 → TLB 翻译 → Store Buffer 查 forwarding → L1 D-Cache 查 tag/读 SRAM → 最后才到 PRF。每一步都可能引入额外延迟。
延迟不固定。L1 hit 4~5 拍,L2 hit +12 拍,L3 hit +40 拍,DDR +200 拍以上——同样的
add eax, [rsi+8],数据到达时刻完全取决于缓存命中率。consumer µop 在保留站里等着,等多久?没人提前知道。前推只能救一部分。ALU→ALU 的前推旁路在第 1 拍就能截到数据(EX/MEM 级间寄存器)。Load 的数据要等 MEM 级甚至更晚才出来——前推网络能看到的最远一条指令(MEM/WB)够不着 Load 的结果。也就是说,Load→Use 的 RAW 天然比 ALU→ALU 多等 2~3 拍以上,而且这还不算 cache miss。
核心问题:consumer µop 怎样在"生产者延迟不确定"的情况下,做到最短等待?答案藏在下图的 LSU 流水线里——每一步的硬件都在努力把数据"早一拍"推给消费者。
1.1 Load µop 的完整流水线(L1 D-Cache 命中)
图例:红色箭头 = 数据依赖关键路径(load 结果如何流入 consumer)。关键路径在消息文字上用 加粗 + 红色 双重标记 + 醒目的浅红背景 note。

1.2 逐拍走读(L1 hit)
阶段一:µop₁ 发射 & 地址生成(拍1)——AGU 双角色并发:算 VA(base + index×scale + offset)的同时做 TLB 翻译,VA 高位匹配 tag 直接得 PPN,合并 offset 成完整 PA,单拍完成。
阶段二:Store Buffer 过滤(拍1,与 TLB 并行)——拿到 PA 后不直接查 D-Cache,先全相联遍历 SB 槽位做地址比对:命中则 Store→Load Forwarding(数据从 SB 直接给 load,最快 ~3 拍);未命中继续查 D-Cache(延迟 +1 拍,SB 查询开销)。
阶段三:L1 D-Cache 查询 & 数据返回(拍2~4)——PA 的 index 位选 set(VIPT 可与 TLB 并行),tag 位和 set 内所有 way 同时比对,命中则 SRAM 读出数据;再经两个周期读出 + 走线延迟,LSU 在第 4 拍收齐数据锁存。
阶段四:µop₁ WB & µop₂ 唤醒(拍5)——数据写入目的 PR(P45),结果总线广播 P45 就绪;保留站 tag 比较器匹配到 µop₂ 的源 tag = P45,µop₂ 立即就绪并发射到 ALU。
阶段五:µop₂ 前推 & 执行(拍6~7)——前推网络发现 P45 正在 µop₁ 的 EX/MEM 输出上,MUX 旁路进 ALU 输入端,跳过 PRF 读省 1 拍;ALU 单拍完成 eax + P45 → P46,µop₂ 拍7 写回。
load→use 最小延迟 = 5 拍(AGU 发射到 USE 发射,L1 hit 场景):
| 场景 | load→use 延迟 | 说明 |
|---|---|---|
| L1 hit + 前推 | 5 拍 | AGU(1) + L1(4) = 5,µop₂ 在下一拍发射 |
| L2 hit | ~17 拍 | +12 拍 L2 访问 |
| L3 hit | ~45 拍 | +40 拍 L3 访问 |
| DDR miss | ~200+ 拍 | ROB 对头阻塞的元凶 |
| store→load forwarding 命中 | 5 拍(同 L1 hit) | 数据从 store buffer 直接给,不走 D-Cache |
即使 L1 命中,load→use 也有 5 拍延迟——依赖链上每条 load→use 都至少拖 5 拍。乱序执行靠保留站把其他不依赖的 µop 填进这 5 拍的缝隙,但如果依赖链全是 load→use(如链表遍历),IPC 直接掉到 1/5 = 0.2。
二、Store → Load RAW:store buffer forwarding 的特殊路径
Store 指令的执行路径和 ALU 完全不同:
- ALU:算出结果写 PRF
- Store:算出地址→写 store buffer→异步刷 D-Cache
程序里常见的 store 后 load 同一地址,产生了一种特殊的 RAW 依赖。
mov [rsi], rax ; store:写 [rsi] ← rax
mov rbx, [rsi] ; load:读 [rsi] → rbx,必须拿到 store 的新值2.1 为什么前推网络救不了 store→load
前推网络管的是寄存器——EX/MEM 级间寄存器里的 ALU 结果旁路给下一条的 ALU 输入端。但 store 写的是内存地址,不是寄存器。store 的数据经过 STA+STD µop 写入 store buffer,不在前推网络覆盖范围内。
2.2 store buffer forwarding 的 µop 级机制
Store 指令被切成 STA + STD 两条 µop,各自写入 Store Buffer 的不同域;后续 load µop 通过地址比对命中这个槽位后直接走 forwarding。

2.3 forwarding 失效的情况
| 失效场景 | 原因 | 惩罚 |
|---|---|---|
| 地址完全匹配 | ✅ 正常 forwarding | 0 额外延迟 |
| 部分重叠(store 写 8B,load 读其中 4B) | 可以部分转发,但数据合并复杂 | ~3~5 拍 |
| 地址不对齐(load 跨两个 cache line) | store buffer 里是连续地址,load 需要两次 forwarding | 拆成两次 load |
| store buffer 中无匹配 | load 地址不在 store buffer 里 | 正常查 L1 D-Cache |
| store 还在 AGU 没完成(STA µop 未发射) | load 不知道 store 的目标地址,无法做地址比对 | load 必须等 STA 完成(memory disambiguation) |
关键坑:forwarding 的效率不取决于 D-Cache 是否命中,只取决于 store buffer 地址比对是否成功。但 store buffer 是按物理地址比对的——同一个虚拟地址的不同进程、或 TLB 未命中导致 PA 不可用,都可能让 forwarding 失效。
2.4 Store Buffer 的 RAW 与 ROB 退休的交互

上图把 Store Buffer 一个槽位的一生拆成四个状态,核心要点藏在三次状态迁移里:
1. 填充(拍4):两条 µop 才能描述一个 store——x86 的 mov [rbx], eax 被拆成 STA + STD 两条 µop,一条写地址、一条写数据,各自独立发射,两条都执行完 SB 槽位的两个域才都填满。在此之前 SB 无法对"半个 store"做 forwarding——所以 load 在 memory disambiguation 阶段发现前面有未完成的 STA,必须 stall 等 STA 完成。
2. 退休(拍6):store"可见",但只对本核——槽位标记 eligible 后:本核 load 走 SB 全相联比对 → forwarding 直接拿数据(完全不碰 D-Cache);其他核 load 看不到这个值(数据还在 SB 里)。x86 的 TSO 内存模型正是靠这个"本核先看到、其他核后看到"的窗口实现的。
3. 仲裁 & 写回(拍 N):从"私有"变成"全局可见"——LSU 仲裁逻辑选"eligible 最久 + D-Cache port 空闲 + cache line coherence 状态允许"的槽位写 L1,写入后其他核通过缓存一致性协议才能看到——从这一刻起 store 才真正"全局可见"。
Store Buffer 的本质:store 指令在退休时不需要等数据落 D-Cache——CPU 把"退休"和"全局可见"解耦了,换来了 store 指令的低延迟(退休不阻塞)。代价就是引入了"本核可见但其他核不可见"的窗口期,这是内存序问题的硬件根源。Store→Load forwarding 是 CPU 在 SB 这里做的补偿——至少让本核自己感觉一切正常。
三、两种访存 RAW 的对照与调优
| 维度 | Load→Use | Store→Load |
|---|---|---|
| 生产者 | L1/L2/L3/DDR | Store Buffer 槽位 |
| 最小延迟 | 5 拍(L1 hit) | ~3~5 拍(SB forwarding) |
| 最坏延迟 | 200+ 拍(DDR miss) | 部分重叠/不对齐时退化 |
| 决定性因素 | 缓存命中率 + MLP | 地址比对是否成功 |
| 典型代码 | a = *p; b = a + 1 | *p = 1; x = *p |
| 调优方向 | 提命中率、提 MLP(见 miss 连锁反应篇) | 避免热路径同地址写读;注意 SB 按物理地址比对 |
两类 RAW 的调优要点并不一样:
- Load→Use 是"缓存战争":延迟大头在数据回来的路上,代码层面能做的是顺序访问(提命中率)、拆独立 load(提 MLP)——cache-friendly-code 的整篇原则都指向这里。
- Store→Load 是"自产自销":只有同一地址的写后读才会触发。绝大多数高性能代码里它不常见;真出现了(比如自旋锁的放锁后读、日志缓冲区的写完即读),先确认地址对齐与完整匹配,再考虑是不是该让 store 走完 L1(
mfence/原子操作)而不是靠 forwarding。
一句话:访存域的两种 RAW,一个拼"数据回来的路"(Load→Use,5~200 拍),一个拼"地址比对的门"(Store→Load,命中 0 额外延迟)——前者的上限是内存延迟,后者的上限是地址匹配的完备性。 它们加上寄存器域的 ALU→ALU 前推(RAW 篇),构成 RAW 的完整图景。
四、一句话总结
Load→Use 走 AGU→TLB→SB→L1 全路径,L1 hit 也至少 5 拍,延迟随命中率浮动到 200+ 拍——乱序执行只能把不依赖的 µop 填进等待缝隙,救不了依赖链本身(链表遍历 IPC 掉到 0.2 就是例证)。Store→Load 则靠 store buffer 地址比对 forwarding:命中零额外延迟,但部分重叠、跨 line 不对齐、STA 未完成都会让它失效。 两者共同点是"数据不在流水线寄存器里",区别是一个比"数据回来多快"、一个比"地址配不配得上"。调优上,Load→Use 拼命中率与 MLP,Store→Load 拼地址对齐与匹配完备性。