﻿# 数据冒险（RAW / WAW / WAR）—— 流水线里指令打架的三种姿势

> 流水线让不同指令的不同阶段重叠执行（见 [pipelining-superscalar.md](/concepts/microarch/pipelining-superscalar.md)），但一重叠就出事：前一条还没写完寄存器，后一条就要读——这就是**数据冒险（Data Hazard）**。根据"谁读、谁写、谁先谁后"，分三种：**RAW（真依赖）、WAW（写后写）、WAR（读后写）**。
>
> **冒险是流水线引起的，不是超标量引起的**——这个区别容易混淆，先讲清楚：
>
> - **流水线是冒险的根因**：把一条指令切成取指/译码/执行/访存/写回五级，不同指令的不同阶段同时跑。`ADD r1,r2,r3` 的写回还没做，下一条 `SUB r4,r1,r5` 已经进执行阶段了——r1 读到的是旧值。哪怕只配标量单发射（IPC ≤ 1），只要流水线在跑，RAW 就会发生。**流水线让"重叠"成为可能，重叠才是冒险的前提**。
> - **超标量放大冒险的影响，但不创造冒险**：同一拍发射 N 条指令，这 N 条之间如果有依赖（第 2 条用第 1 条的结果），顺序发射逻辑会让第 2 条 stall——同一拍里的 N 条本该并行，但 RAW 把它们打回了串行。超标量让"依赖密度"变高（同一拍内出现依赖的概率远大于流水线里跨拍的依赖），但依赖本身不因超标量而诞生——标量流水线照样有 RAW，超标量只是让**同一个 RAW 同时堵住更多执行槽位**。
> - **乱序执行是被流水线+超标量逼出来的**：流水线制造冒险 → 超标量加剧冒险对吞吐的伤害 → 乱序执行 + 重命名 + 保留站这套机制就是为此而生的解法。所以三者不是并列关系，是**流水线（根因）→ 超标量（放大）→ 乱序（解法）**的因果链。
>
> **先划清性质**：RAW 是**正确性问题**——consumer 读早了会拿到错误的值，硬件必须保证指令间的依赖顺序不被流水线打破。WAW 和 WAR 在有寄存器重命名的现代核心里，已经被重命名阶段消除了正确性风险，但会导致**性能问题**——重命名表(RAT)无空闲 PR 时流水线 stall，以及粗暴解法（in-order 发射）带来的 IPC 损失。
>
> 它们不是同等级别的麻烦——RAW 是真正绕不开的依赖，WAW 和 WAR 是"名字复用"造成的假象，硬件有完全不同的解法。**本文以 µop 为粒度，细致到 store buffer / load buffer / L1 D-Cache 流水线逻辑**，把每种冒险在硬件里到底怎么发生的讲透。

## 一、从 µop 视角重新定义三种冒险

x86 是 CISC 指令集，但到了硬件层面，**CISC 只暴露到译码器为止**。译码器的职责就是把一条复杂的 x86 指令拆成一到多条 µop，之后的所有流水线部件——重命名、保留站、执行单元、LSU、ROB——处理的全是 RISC 风格的 µop，与它们来自 x86 还是 ARM 指令集毫无关系：

- **汇编层**：程序员看到的是 `add eax, [rsi+8]`，一条 CISC 指令同时干了"读内存"和"做加法"
- **译码器（CISC→µop 边界）**：拆成 `LD tmp, [rsi+8]` + `ADD eax, tmp`，一条变两条
- **执行层（全部 RISC）**：µop 固定三操作数 `dest, src1, src2`，只做一件事（load 只 load，ALU 只算），寄存器全部是物理寄存器编号，再也没有"累加器既是源又是目的"这回事

所以冒险本质上发生在 **µop 与 µop 之间**，而不是 x86 指令与指令之间：

```bash
┌─────────────────────────────────────────────────────────────────┐
│  一条 x86 指令可能拆成多条 µop：                                    │
│                                                                 │
│  mov [rbx], eax        → STA [rbx]  +  STD eax                  │
│                           (Store Addr)   (Store Data)            │
│                           µop₁ 无目的PR  µop₂ 无目的PR             │
│                                                                 │
│  add eax, [rsi+8]      → LD tmp,[rsi+8] + ADD eax,tmp           │
│                           µop₁ 写目的PR   µop₂ 读µop₁的目的PR      │
│                           ↑ 这两条 µop 之间有 RAW！              │
│                                                                 │
│  addsd xmm0, [rdi+rcx*8] → LD tmp,[rdi+...] + ADDSD xmm0,tmp    │
│                            µop₁ 在 INT RS   µop₂ 在 FP RS       │
│                            ↑ 跨集群 RAW！                        │
└─────────────────────────────────────────────────────────────────┘
```

> **冒险 = µop 之间的物理寄存器依赖**。程序里写的是 `eax`，µop 之间传递的是物理寄存器号（如 PR12→PR37）。冒险检测和消解都在物理寄存器域完成。

## 二、RAW（Read After Write）：真依赖——硬边界，绕不过

### 2.1 是什么

一条 µop **读**的物理寄存器，恰好是前面一条 µop 要**写**的物理寄存器。消费者的输入 = 生产者的输出——这是真实的、无法消除的数据流依赖。

```bash
µop₁: ADD P37, P12, P23     ; 写目的 PR = P37（结果）
µop₂: SUB P45, P37, P30     ; 读源 PR = P37 ← 必须等 µop₁ 写完
```

µop₂ 读 P37 不是碰巧同名——µop₂ 就是要用 µop₁ 算出来的值。**这是真依赖，硬件必须保证 µop₂ 读到的是 µop₁ 写入后的值。**

但"前面一条"的生产者身份不同，RAW 的严重程度也完全不同。按**数据从哪里来**，RAW 分三种：

| 类型 | 生产者 | 数据延迟 | 前推能不能救 | 典型场景 |
|------|--------|:--:|:--:|------|
| **ALU→ALU** | ALU 计算结果 | 1 拍 | ✅ 前推旁路直接截到 | `a=b+c; d=a+e` |
| **Load→Use** | L1 D-Cache / 下级缓存 | 4~200+ 拍 | ⚠️ 只能减 1~2 拍，大头在 cache 延迟 | `a = *p; b = a + 1` |
| **Store→Load** | Store Buffer forwarding | ~3~5 拍 | ✅ SB forwarding 等价于前推 | `*p=1; x=*p`（同地址） |

三种 RAW 的本质都是"消费者等生产者"，但等的时长差了两个数量级：

- **ALU→ALU**：生产者 1 拍就算完了，前推网络在数据"还没落位 PRF"时就截走了，consumer 几乎不用等——这是硬件设计最优雅的部分
- **Load→Use**：生产者的数据不在流水线里——它在外面的缓存层次，4 拍是起跑线，200 拍也不稀奇。前推只能省 PRF 读的那 1 拍，杯水车薪
- **Store→Load**：特殊在"生产者"不是 ALU 也不是 cache，而是 store buffer 里还没落位的 store 数据——地址比对命中了直接给，命不中才走 D-Cache

> 下面按"从简单到复杂、从快到慢"展开：先看前推能完美解决的 ALU→ALU，再看延迟不固定的 Load→Use，最后看 Store Buffer 参与的 Store→Load forwarding。

### 2.2 最简单的 RAW：ALU → ALU（前推旁路，差 1 拍）

两条纯算术 µop，生产者和消费者紧挨着：`µop₁: ADD P37, P12, P23` → `µop₂: SUB P45, P37, P30`

```plantuml
@startuml
title ALU→ALU RAW：无前推 vs 有前推对比

participant "IF/ID" as FE
participant "REN" as REN
participant "RS" as RS
participant "ALU" as ALU
participant "EX/MEM\n级间寄存器" as EXMEM
participant "PRF" as PRF

== µop1 (ADD P37, P12, P23) ==
FE -> REN: µop1 取指/译码
REN -> RS: µop1 重命名 (分配 P37)
RS -> ALU: 拍4: µop1 发射
activate ALU
ALU -> ALU: P12 + P23 → 结果
ALU -> EXMEM: 结果锁存到 EX/MEM
note right #FFFDE7: ALU 输出时刻：结果已算出\n但尚未写入 PRF
deactivate ALU

== µop2 (SUB P45, P37, P30) — 源 P37 tag 非零 ==
FE -> REN: µop2 取指/译码 (比 µop1 晚 1 拍)
REN -> RS: <color:red><b>µop2 重命名<br>源 P37 tag≠0，在 RS 等待</b></color>

== 前推旁路：省 1 拍 ==
RS -> ALU: 拍5: µop2 发射\n(P37 tag 匹配 → 唤醒)
activate ALU
EXMEM --> ALU: 前推网络旁路\n直接给 µop2 的 MUX 输入
note right #C8E6C9: MUX 三路来源：\n① PRF 常规读\n② EX/MEM 前推 ← 命中\n③ MEM/WB 前推
ALU -> PRF: 拍6: µop2 WB\nP45 写入 PRF
deactivate ALU

note over ALU, EXMEM #E3F2FD
  **无前推**：µop2 必须等 µop1 WB 写回 PRF 再读 → µop2 EX 在拍6 → 差 2 拍
  **有前推**：µop2 EX 在拍5 直接从 EX/MEM 拿值 → 差 1 拍（背靠背）
  tag 比较器检测 "源 PR tag == 前面 µop 目的 PR tag" → 匹配则选旁路
end note

@enduml
```

图上三条泳道完整呈现了 µop₂ 从取指到写回的 6 个步骤，核心问题只有三个：

1. **P37 还没算出来，µop₂ 就要用——硬件怎么知道"还没好"？**
   重命名阶段，RAT（寄存器别名表）查到 P37 对应的目的 PR 还在流水线里没写回 → 给 µop₂ 的源操作数标记 `tag≠0`。µop₂ 带着这个 tag 进入保留站。

2. **µop₂ 怎么知道 P37"什么时候好"？**
   保留站逐拍监听结果总线的广播——当 µop₁ 的 EX 级算完，结果总线广播 `PR37 就绪`，保留站里的 tag 比较器匹配到 `tag=37`，立即唤醒 µop₂。

3. **µop₂ 怎么"拿到" P37 的值——数据从哪来？**
   不是等 µop₁ WB 写回 PRF 再读（那要等到拍6），而是 ALU 输入端的 MUX 在发射时直接比较：µop₂ 的源 PR tag 和 EX/MEM、MEM/WB 的目的 PR tag 做匹配——命中 EX/MEM → MUX 选中级间寄存器的输出，数据"插队"进入 ALU，省 1 拍。

> 三个问题对应三个硬件机制：**RAT（判断依赖）→ 结果总线广播（唤醒等待者）→ 前推 MUX（截胡数据）**。缺一个，RAW 惩罚都会扩大。

> **前推能消除几乎所有 ALU→ALU 的 RAW 代价**——从差 2 拍压到差 1 拍（背靠背），保留站里的 tag 匹配 + 前推 MUX 组合是标准解法。

#### 2.2.1 前推的硬件是怎么做的——tag 匹配 + MUX 选路

ALU→ALU RAW 能在 1 拍内消解，靠的是两件事同时发生：

**① 保留站的 tag 唤醒**。每条 µop 在发射到保留站时，源操作数如果是未就绪的（RAT 查到目的 PR 还没算出来），保留站槽位里写的是"等哪个 PR"——比如 µop₂ 的源 P37 未就绪，槽位里记 `tag=37`。当 µop₁ 在 EX 级结束、结果锁存到 EX/MEM 级间寄存器的同时，结果总线广播 `PR37 就绪`。保留站里的 tag 比较器逐拍监听这条广播，匹配到 tag=37 → 立即唤醒槽位里的 µop₂，调度器在同一拍或下一拍把 µop₂ 发射到 ALU。

**② ALU 输入端的 MUX 选路**。µop₂ 发射进 ALU 时，ALU 的输入不是只能从 PRF（物理寄存器文件）读——它前面有一排 MUX，三路来源：

| 来源 | 延迟 | 命中条件 |
|------|:--:|------|
| PRF 常规读 | 0（基准） | 默认路径，tag 匹配不上时走这路 |
| EX/MEM 前推 | **-1 拍** | 正在执行的指令（上一条）的目的 PR 匹配 |
| MEM/WB 前推 | **-1 拍** | 上上条指令的目的 PR 匹配 |

µop₂ 发射时，比较器同时把 µop₂ 的源 PR tag 和 EX/MEM、MEM/WB 的目的 PR tag 做对比——匹配到 EX/MEM，则 MUX 直接选 EX/MEM 级间寄存器的输出，跳过 PRF 读。这就是图里 EXMEM 到 ALU 的虚线箭头——**数据在级间寄存器里"插队"送到下一条 ALU 输入端**。

> **为什么前推网络只向前看 1~2 条？** 因为更早的指令早就 WB 写回 PRF 了，直接走 PRF 常规读就行。前推的本质是"截胡还没落位的数据"——只截最近 1~2 条，足够了。

### 2.3 最典型的 RAW：Load → Use（AGU→TLB→L1 D-Cache→WB 全路径，至少 4~5 拍）

这才是真实代码里最常见的 RAW 瓶颈。Load µop 的数据不是 ALU 1 拍就能算出来的——它要走完 LSU 的完整流水线：

```asm
add eax, [rsi+8]    ; 拆成两条 µop
                     ; µop₁: LD  tmp, [rsi+8]     ← 数据要等 MEM 级才出来
                     ; µop₂: ADD eax, tmp          ← 必须等 µop₁
```

和 ALU→ALU 比，Load→Use 的 RAW 有三层**不一样**：

1. **数据不是 ALU 算出来的**。ALU→ALU 只有一个"谁生产、谁消费"的问题，数据在 ALU 里 1 拍就算完。Load 的数据来自内存层次——AGU 算地址 → TLB 翻译 → Store Buffer 查 forwarding → L1 D-Cache 查 tag/读 SRAM → 最后才到 PRF。每一步都可能引入额外延迟。

2. **延迟不固定**。L1 hit 4~5 拍，L2 hit +12 拍，L3 hit +40 拍，DDR +200 拍以上——同样的 `add eax, [rsi+8]`，数据到达时刻完全取决于缓存命中率。consumer µop 在保留站里等着，等多久？没人提前知道。

3. **前推只能救一部分**。ALU→ALU 的前推旁路在第 1 拍就能截到数据（EX/MEM 级间寄存器）。Load 的数据要等 MEM 级甚至更晚才出来——前推网络能看到的最远一条指令（MEM/WB）够不着 Load 的结果。也就是说，Load→Use 的 RAW **天然比 ALU→ALU 多等 2~3 拍以上**，而且这还不算 cache miss。

> **核心问题**：consumer µop 怎样在"生产者延迟不确定"的情况下，做到最短等待？答案藏在下图的 LSU 流水线里——每一步的硬件都在努力把数据"早一拍"推给消费者。

#### 2.3.1 Load µop 的完整流水线（L1 D-Cache 命中）

> **图例**：红色箭头 = 数据依赖关键路径（load 结果如何流入 consumer）。关键路径在消息文字上用 **加粗 + 红色** 双重标记 + 醒目的浅红背景 note。

```plantuml
@startuml
title Load µop 完整流水线：LSU 各组件交互

participant "RS" as RS
participant "AGU\nPort 2/3" as AGU
participant "TLB" as TLB
participant "Store\nBuffer" as SB
participant "L1\nD-Cache" as L1
participant "L2/L3/\nDRAM" as MEM
participant "PRF" as PRF

RS -[#666666]> AGU: 发射 µop₁(LD)\n调度器挑中 Port 2/3 AGU
activate AGU
AGU -[#666666]> AGU: base+index*scale+offset → VA
note right: 1 拍

AGU -[#666666]> TLB: VA → PA 翻译
activate TLB
alt <color:#4CAF50>✅ TLB hit（正常路径）</color>
  TLB -[#4CAF50]> AGU: PA 返回 (1 拍)
  deactivate TLB
  note right #C8E6C9: ✅ TLB hit，延迟 ~1 拍

  AGU -[#666666]> SB: 用 PA 查 Store Buffer
  activate SB

  alt <color:#FF9800>⚡ Store Buffer 完全匹配</color>
    SB -[#FF9800]> AGU: Store→Load Forwarding!\n数据直接给 load
    deactivate SB
    deactivate AGU
    note right #FFF3E0: ⚡ SB forwarding：~3 拍\n走 SB，不走 D-Cache
  else <color:#666666>Store Buffer 不匹配</color>
    deactivate SB
    AGU -[#666666]> L1: 发起 L1 D-Cache 访问\nPA index→set, tag→way
    activate L1

    alt <color:#4CAF50>✅ L1 hit（正常路径）</color>
      L1 -[#D32F2F]> AGU: <color:#D32F2F><b>【关键】数据返回 ~4 拍</b></color>\nload 结果 <b>RAW 依赖起点</b>
      deactivate L1
      note right #FFCDD2: 🔴 <b>数据依赖关键路径</b>\nload 结果从 L1 流出\n这是 RAW 依赖的起点

    else <color:#FF9800>⚠ L1 miss</color>
      AGU -[#FF9800]> MEM: 发起 L2 请求
      activate MEM
      note right #FFF3E0: ⚠ L1 miss，开始查下级 cache

      alt <color:#FF9800>⚠ L2 hit</color>
        MEM -[#FF9800]> AGU: <color:#FF9800><b>L2 hit 数据 (+12 拍)</b></color>
        note right #FFF3E0: ⚠ L2 hit：额外 +12 拍

      else <color:#EF5350>L2 miss</color>
        alt <color:#EF5350>🔴 L3 hit</color>
          MEM -[#EF5350]> AGU: <color:#EF5350><b>L3 hit 数据 (+40 拍)</b></color>
          note right #FFCDD2: 🔴 L3 hit：额外 +40 拍

        else <color:#D32F2F>🔴 L3 miss → DDR</color>
          MEM -[#D32F2F]> AGU: <color:#D32F2F><b>DDR 数据 (+200+ 拍)</b></color>
          note right #FFCDD2: 🔴 DDR 访问：额外 +200+ 拍

        end
      end
      deactivate MEM

    end
  end
else <color:#D32F2F>🔴 TLB miss（异常路径）</color>
  TLB -[#D32F2F]> MEM: Page Walk (几十~几百拍)
  deactivate TLB
  note right #FFCDD2: 🔴 TLB miss → Page Walk\n延迟 几十~几百拍
end

deactivate AGU

AGU -[#7E57C2]> PRF: WB: 数据写入目的 PR
activate PRF
note right #EDE7F6: 结果总线阶段
PRF -[#7E57C2]> RS: 结果总线广播 tag\n等待中的 µop₂ 被唤醒
deactivate PRF

@enduml
```

#### 2.3.2 逐拍走读（L1 hit）

##### 阶段一：µop₁ 发射 & 地址生成（拍1）

AGU 的两个角色：**地址计算** + **地址翻译**，一根拍内并发完成。

- **AGU 算 VA**：`base + index×scale + offset` → 线性地址，单拍完成
- **TLB 译 PA**（并行）：VA 的高位做 TLB tag 匹配，命中则直接返回 PPN（物理页号），合并 offset 得到完整 PA

##### 阶段二：Store Buffer 过滤（拍1，与 TLB 并行）

拿到 PA 后，不直接查 D-Cache——先遍历 Store Buffer 所有槽位做**全相联地址比对**。

| 匹配结果 | 行为 | 说明 |
|----------|------|------|
| SB 命中 | Store→Load Forwarding | 数据从 SB 槽位直接给 load，不走 D-Cache，最快 ~3 拍 |
| SB 未命中 | 继续查 D-Cache | 正常路径，延迟 +1 拍（SB 查询开销） |

> SB 和 TLB 互不阻塞——TLB 翻译在上游、SB 查询在下游，两路并发。

##### 阶段三：L1 D-Cache 查询 & 数据返回（拍2~4）

D-Cache 用 PA 分两步寻址：

1. **Index 选 set**：PA 的 index 位直接选中对应的 cache set（VIPT 可和 TLB 并行，但完整的 tag 比较需等 PA）
2. **Tag 多路比较**：PA 的 tag 位和 set 内所有 way 的 tag 同时比对——命中 → SRAM 读出数据

接下来两个周期是 D-Cache **SRAM 读出 + 走线延迟**，LSU 在第 4 拍收齐数据并锁存到内部寄存器。

##### 阶段四：µop₁ WB & µop₂ 唤醒（拍5）

- **µop₁ WB**：锁存的数据写入目的 PR（P45），结果总线广播 `P45 就绪`
- **µop₂ 唤醒**：保留站 tag 比较器检测到 µop₂ 的源操作数 tag = P45 → 匹配成功 → µop₂ 立即变为就绪态
- **发射**：调度器在本拍（或最晚下一拍）将 µop₂ 发射到 ALU

##### 阶段五：µop₂ 前推 & 执行（拍6~7）

µop₂ 发射进 ALU 时，前推网络检查：

| 前推来源 | 匹配条件 | 命中 |
|----------|----------|:--:|
| EX/MEM 级间寄存器 | µop₁(P45) == 上一条的目的 PR | **← 命中** |
| MEM/WB 级间寄存器 | µop₁(P45) == 上上条的目的 PR | — |
| PRF 常规读 | 以上都未命中 | — |

前推网络检测到 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。

### 2.4 Store → Load RAW：store buffer forwarding 的特殊路径

Store 指令的执行路径和 ALU 完全不同：
- ALU：算出结果写 PRF
- Store：算出地址→写 store buffer→异步刷 D-Cache

程序里常见的 store 后 load 同一地址，产生了一种特殊的 RAW 依赖。

```asm
mov [rsi], rax      ; store：写 [rsi] ← rax
mov rbx, [rsi]      ; load：读 [rsi] → rbx，必须拿到 store 的新值
```

#### 2.4.1 为什么前推网络救不了 store→load

前推网络管的是**寄存器**——EX/MEM 级间寄存器里的 ALU 结果旁路给下一条的 ALU 输入端。但 store 写的是**内存地址**，不是寄存器。store 的数据经过 STA+STD µop 写入 **store buffer**，不在前推网络覆盖范围内。

#### 2.4.2 store buffer forwarding 的 µop 级机制

Store 指令被切成 STA + STD 两条 µop，各自写入 Store Buffer 的不同域；后续 load µop 通过地址比对命中这个槽位后直接走 forwarding。

```plantuml
@startuml
title Store→Load RAW：store buffer forwarding 全流程

participant "RS" as RS
participant "AGU" as AGU
participant "Store Data\nPort 4" as STD_PORT
participant "Store\nBuffer" as SB
participant "TLB" as TLB
participant "L1\nD-Cache" as L1
participant "PRF" as PRF

== Phase 1: Store 指令：STA + STD 填充 Store Buffer ==
RS -> AGU: STA µop 发射\n源 rbx → 算地址
activate AGU
AGU -> SB: 地址 0x7fff1000 → 槽位 #3 地址域
deactivate AGU
note right: STA 不写目的 PR，不广播 tag

RS -> STD_PORT: STD µop 发射\n读 eax 的 PR 值
activate STD_PORT
STD_PORT -> SB: 数据 0x2a → 槽位 #3 数据域
deactivate STD_PORT
note right: STD 不写目的 PR，不广播 tag

note over SB #C8E6C9
  **Store Buffer 槽位 #3 已填充**
  地址=0x7fff1000, 数据=0x2a
  状态：等待 STA+STD 退休
end note

== Phase 2: Load µop 查 Store Buffer → forwarding ==
RS -> AGU: LD  µop 发射\n源 rsi=0x7fff1000
activate AGU
AGU -> TLB: VA=0x7fff1000 → PA=0x1a001000
activate TLB
TLB --> AGU: PA=0x1a001000
deactivate TLB

AGU -> SB: 用 PA 逐条比对\n所有槽位地址域
activate SB
SB --> AGU: **槽位 #3 命中！**\nforwarding 触发：0x2a
deactivate SB
note right #C8E6C9: Store→Load Forwarding\n数据来自 SB，不走 D-Cache

AGU -> PRF: WB: P45 ← 0x2a
deactivate AGU
PRF -> RS: tag 广播：P45 就绪！

@enduml
```

#### 2.4.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.4 Store Buffer 的 RAW 与 ROB 退休的交互

```plantuml
@startuml
title Store Buffer 生命周期：从分配到释放

participant "REN" as REN
participant "RS" as RS
participant "AGU" as AGU
participant "Store\nBuffer" as SB
participant "ROB" as ROB
participant "LSU" as LSU
participant "L1\nD-Cache" as L1

== 拍2：分配 ==
REN -> SB: 申请 SB 槽位 #3
SB --> REN: 分配 #3 → 状态：<color:#FF9800><b>空</b></color>
REN -> RS: STA + STD µop 入保留站\n携带 SB 槽位号 #3

== 拍4：执行 ==
RS -> AGU: 发射 STA µop
activate AGU
AGU -> AGU: base + offset → VA
AGU -> SB: 写入地址域 (PA)\n槽位 #3
deactivate AGU

RS -> AGU: 发射 STD µop
activate AGU
AGU -> SB: 写入数据域 (value)\n槽位 #3
deactivate AGU
note right #FFF3E0: 状态：<color:#FF9800><b>已填充</b></color>\n地址+数据双字段就绪

== 拍6：退休 ==
ROB -> SB: STA + STD 对端退休\n槽位 #3 标记可提交
note right #C8E6C9: 状态：<b>可提交 (eligible)</b>

== 拍 N (N≥6)：仲裁 & 写回 ==
LSU -> SB: 仲裁选中槽位 #3
activate LSU
LSU -> L1: PA 索引 + 数据写入\nD-Cache 对应 set/way
activate L1
L1 --> LSU: 写入确认
deactivate L1
LSU -> SB: 释放槽位 #3
deactivate LSU
note right #FFCDD2: 状态：<b>已提交 → 释放</b>

note over SB #E3F2FD
  <b>★ 关键窗口</b>
  拍4~6：load 通过 store→load forwarding 能拿到这个值
  拍6~N：其他核看不到（store buffer 里，未写入 D-Cache）
  拍 N 后：其他核可见
end note

@enduml
```

上图把 Store Buffer 一个槽位的一生拆成四个状态，核心要点藏在三次状态迁移里：

**1. 填充（拍4）：两条 µop 才能描述一个 store**

x86 的 `mov [rbx], eax` 被译码器拆成 STA + STD 两条 µop——一条写地址、一条写数据，各自独立发射。两条都到了 AGU 执行完，SB 槽位的两个域才都填满。在此之前（哪怕地址域先到了），SB 比对逻辑无法对"半个 store"做 forwarding——这就是为什么 load 在 memory disambiguation 阶段如果发现前面有未完成的 STA，**必须 stall 等 STA 完成**。

**2. 退休（拍6）：store 是"可见"的，但只对本核**

STA+STD 退休后槽位标记 eligible，此时：
- **本核的 load**：SB 全相联比对 → 地址匹配 → forwarding 直接给数据，完全不走 D-Cache。这就是 2.4 节讲的 store→load forwarding
- **其他核的 load**：看不到这个值——数据还在 SB 里，D-Cache 里没有。这意味着**多核之间，store 不是退休那一刻对其他核可见的**，×86 的 TSO 内存模型正是靠这个"本核先看到、其他核后看到"的窗口实现的

**3. 仲裁 & 写回（拍 N）：从"私有"变成"全局可见"**

LSU 的仲裁逻辑决定哪个 SB 槽位可以写 D-Cache——不是 FIFO，而是根据"哪个槽位 eligible 最久 + 当前 D-Cache port 空闲 + 对应 cache line 的 coherence 状态允许"。一旦写入 L1，其他核通过缓存一致性协议就能看到这个值——**从这一刻起 store 才真正"全局可见"**。

> **Store Buffer 的本质**：store 指令在退休时不需要等数据落 D-Cache——CPU 把"退休"和"全局可见"解耦了，换来了 store 指令的低延迟（退休不阻塞）。代价就是引入了"本核可见但其他核不可见"的窗口期，这是内存序问题的硬件根源。Store→Load forwarding 是 CPU 在 SB 这里做的补偿——至少让本核自己感觉一切正常。

## 三、WAW（Write After Write）：写后写——有两种，寄存器 WAW 和内存 WAW

> WAW 分两个完全不同的域：**寄存器 WAW** 发生在物理寄存器号上，重命名阶段直接消除；**内存 WAW** 发生在同一 cache line 地址上，靠 ROB + store buffer 提交顺序保证——重命名管不到，硬件机制也完全不同。下面分开讲。

### 3.1 寄存器 WAW：重命名直接消除（正确性问题 → 重命名后不存在）

两条 µop 写同一个**架构寄存器**，经过重命名之前会争用同一个物理寄存器号，但它们的值彼此独立：

```bash
架构寄存器视角：
  ① add rax, rbx     ; rax ← rbx + 旧rax
  ② mov [mem], rcx   ; 不碰 rax
  ③ add rax, rdx     ; rax ← rdx + 新rax

µop 视角（没有重命名）：
  µop₁(①): ADD P10, P12, P23    ; 写 PR P10（rax 的映射）
  µop₂(②): STA [mem]            ; 不写寄存器
  µop₃(②): STD rcx              ; 不写寄存器
  µop₄(③): ADD P10, P10, P20    ; 写 PR P10 ← 但和 µop₁ 写的是同一个 PR！
  ↑ µop₁ 和 µop₄ 都写 P10，WAW 冲突
```

如果 µop₄ 比 µop₁ 先执行完（乱序），P10 先收到 µop₄ 的值，然后 µop₁ 写 P10 覆盖了 µop₄ 的值——最终 `rax` 里是旧的 µop₁ 结果，程序逻辑错误。

**怎么解——重命名，各写各的 PR：**

```bash
重命名后：
  µop₁(①): ADD P37, P12, P23    ; 写 PR P37（分配新 PR）
            RAT 记录：rax → P37（旧映射 P10 暂存 ROB）
  µop₄(③): ADD P52, P37, P20    ; 写 PR P52（分配新 PR）
            RAT 更新：rax → P52（P37 暂存 ROB）
  ★ P37 和 P52 是两个独立的物理寄存器，"先后覆盖"不存在
```

**没有重命名时，问题在哪？** µop₁ 和 µop₄ 的"写目标"在 µop 里直接编码为 `ADD P10, ...`——同一个物理寄存器号 P10。乱序核里如果 µop₄ 先执行完，P10 被写入了 µop₄ 的新值；然后 µop₁ 才执行完，P10 又被写入 µop₁ 的旧值——新值被旧值覆盖，最终 `rax` 读到的是错误的旧结果。

**重命名怎么解的？** 核心动作只有两步：

1. **给每个"写"分配一个新 PR**。µop₁ 经过重命名时，RAT 看到 `rax` 当前映射是 P10，分配新 PR P37，µop₁ 的目的 PR 从 "P10" 改写为 "P37"。RAT 更新 `rax → P37`。等 µop₄ 经过重命名时，RAT 看到 `rax` 当前映射是 P37，再分配新 PR P52，µop₄ 的目的 PR 改写为 "P52"。RAT 再次更新 `rax → P52`。

2. **RAT 始终指向"最新版本"**。不管 µop₁ 和 µop₄ 谁先执行完，物理寄存器 P37 和 P52 是两个独立的存储单元，互不覆盖。后续任何读 `rax` 的 µop，重命名时查 RAT 得到 `rax → P52`（最新映射），读的就是 µop₄ 的结果——这就是程序序里应该看到的正确值。

> 关键：**RAT 是一个"最新版本号表"**。它不记录所有历史版本，只记录每个架构寄存器当前映射到哪个 PR。而旧映射（如 `rax → P10`、`rax → P37`）被暂存到 ROB 里——如果 µop₄ 是投机路径、需要回滚，ROB 在回滚时把 RAT 恢复到 µop₄ 之前的映射 `rax → P37`。

> 详细机制见 **[register-renaming.md](/concepts/microarch/register-renaming.md)**。寄存器 WAW 只要给每个写分配不同的目的 PR 就完全消除——这是重命名最直接的价值，也是寄存器 WAW 在*正确性*层面其实并不危险的原因：所有现代 x86 核心都有重命名，寄存器 WAW 退化成了 RAT 资源紧张时的性能问题（无空闲 PR 则 stall），而非正确性问题。

### 3.2 Store WAW：地址相同的 store→store，重命名管不到

这是**内存域**的 WAW——两条 store 写同一个内存地址，最终内存里必须是后一条的值。

```asm
mov [rsi], rax      ; store₁：写 [rsi] ← rax
mov [rsi], rbx      ; store₂：写 [rsi] ← rbx（同一地址）
```

store₁ 和 store₂ 都写 `[rsi]`，最终内存里必须是 store₂ 的值。

**硬件怎么保证**：
1. store₂ 的 STA µop 在 AGU 阶段算地址时，store 队列里有更早的 store₁（同一 ROB 顺序但更早分配），LSU 检测到地址冲突。
2. store₂ 必须等 store₁ 退休（commit 到 store buffer 可提交状态）后才能写自己的数据到 store buffer。
3. 或者更常见的实现：允许并行写 store buffer 的不同槽位，但 LSU 按 ROB 顺序把 store buffer 提交到 L1 D-Cache——store₂ 的槽位不会在 store₁ 之前提交。

> **Store WAW 的重排序被 ROB + store buffer 的提交顺序保障，不需要额外硬件。**但注意，这只是单核内的保序——多核视角下，其他核在 store buffer 提交到 D-Cache 之前看不到任何 store 的值。

## 四、WAR（Write After Read）：读后写——同样分寄存器 WAR 和内存 WAR

> 和 WAW 一样，WAR 也分两个域：**寄存器 WAR** 靠重命名消除（各写各的 PR，不会覆盖被读的 PR）；**内存 WAR** 是 load→store 地址冲突，靠 memory disambiguation 保证 load 读到旧值后才允许 store 写入。下面分开讲。

### 4.1 寄存器 WAR：重命名直接消除

前面 µop **读**某个架构寄存器，后面 µop **写**同一个架构寄存器。后写的不能在前读完成之前执行（否则前读拿不到新值而非旧值）：

```bash
架构寄存器视角：
  ① mov [mem], rax      ; 读 rax（旧值）→ 存到内存
  ② add rax, rbx        ; 写 rax（新值）

µop 视角（没有重命名）：
  µop₁(①): STA [mem]    ; 不读 rax，不写寄存器
  µop₂(①): STD rax      ; 读 rax 的 PR（如 P10）→ store buffer
  µop₃(②): ADD P10, P10, P20  ; 写 P10 ← 新值
  ↑ µop₂ 读 P10，µop₃ 写 P10 → WAR 冲突
```

如果 µop₃ 比 µop₂ 先执行完，µop₂ 读 P10 时拿到的就是 µop₃ 的新值——store 写入了错误数据。

### 4.2 寄存器 WAR 的解法：同样是重命名

```bash
重命名后：
  µop₂(①): STD 源=P10          ; 读 P10（旧 rax 值）
  µop₃(②): ADD P52, P10, P20   ; 写 P52（新 rax 值），不碰 P10
  ★ µop₂ 读 P10、µop₃ 写 P52 —— 完全不同的物理寄存器，没有冲突
```

> 和寄存器 WAW 同理：有重命名的核心里，寄存器 WAR 在正确性层面不存在，只会在极端寄存器压力下成为性能瓶颈。

### 4.3 Load→Store WAR：内存域的 WAR，重命名管不到

这是**内存域**的 WAR——load 读内存地址、后面 store 写同一地址，按程序序 load 应该读到 store 写入前的旧值：

```asm
mov rbx, [rsi]      ; load₁：读 [rsi] → rbx
mov [rsi], rax      ; store₂：写 [rsi] ← rax
```

按程序序，load₁ 应该读到 store₂ 写入之前的旧值。如果乱序执行让 store₂ 先完成（写入了新值），load₁ 读到的是新值而非旧值——程序逻辑错误（load₁ 应该读旧值、store₂ 写新值）。

**硬件怎么保证**（memory disambiguation）：
- LSU 的 memory disambiguation predictor 记录历史上哪些 load-store 对发生过地址冲突。
- load₁ 的地址算出后，比对前面未执行完的 store₂——如果地址相同，load₁ 必须等 store₂ 完成（否则读到新值）。
- 如果投机执行 load₁（预测地址不同），后来发现冲突 → pipeline flush + 重放 load₁。

> 这部分的更多细节见 [lsu.md §3.4](/concepts/microarch/lsu.md) 的 memory disambiguation。

## 五、三种冒险对比总结（µop 级）

```plantuml
@startuml
title µop 级三种冒险分类全景

rectangle "**RAW（真依赖）**\n── 不可消除，必须调度 ──" as RAW #E8F5E9 {
  rectangle "ALU→ALU\n前推旁路，1拍延迟" as R1 #C8E6C9
  rectangle "Load→Use\nAGU→TLB→SB→L1\n5拍（L1 hit）" as R2 #C8E6C9
  rectangle "Store→Load\nSB forwarding\n地址匹配 0 额外" as R3 #C8E6C9
  rectangle "长延迟→Use\nFMA 4~5 / FP DIV 20+\nDDR 200+，RS tag 监控" as R4 #C8E6C9
}

rectangle "**WAW（假依赖）**\n── 可消除 ──" as WAW #FFF3E0 {
  rectangle "寄存器 WAW\n重命名 → 各写各 PR\n完全消除" as W1 #FFECB3
  rectangle "Store WAW\nROB 顺序\nSB 按序提交 L1 D-Cache" as W2 #FFECB3
}

rectangle "**WAR（假依赖）**\n── 可消除 ──" as WAR #E3F2FD {
  rectangle "寄存器 WAR\n重命名 → 读写不同 PR\n完全消除" as WR1 #BBDEFB
  rectangle "Load→Store WAR\nmemory disambiguation\npredictor + flush" as WR2 #BBDEFB
}
@enduml
```

| | RAW（Read After Write） | WAW（Write After Write） | WAR（Write After Read） |
|---|---|---|---|
| **读/写顺序** | 先写后读 | 先写后写 | 先读后写 |
| **依赖性质** | **真依赖**（consumer 需要 producer 的结果）| **假依赖**（碰巧叫同一个名字）| **假依赖**（碰巧叫同一个名字）|
| **µop 层面** | µop₂ 的源 PR tag = µop₁ 的目的 PR tag | µop₁ 和 µop₂ 都写同一个架构寄存器（经重命名前） | µop₁ 读、µop₂ 写同一个架构寄存器 |
| **能否消除** | **不能**——真依赖是程序数据流语义 | **能**——寄存器重命名，各写各的 PR | **能**——寄存器重命名，读写不同 PR |
| **L1 D-Cache 是否涉及** | **是**——Load→Use 和 Store→Load 都走 LSU/D-Cache | 寄存器 WAW 不涉及；Store WAW 涉及 | Load→Store WAR 涉及 memory disambiguation |
| **Store Buffer 是否涉及** | **是**——Store→Load RAW 走 store buffer forwarding | **是**——Store WAW 的保序依赖 store buffer | 否 |
| **硬件解法** | 前推 + 保留站 tag 监控 + store buffer forwarding | 寄存器重命名；store buffer 按 ROB 序提交 | 寄存器重命名；memory disambiguation |

### 5.1 为什么 RAW 无法消除而 WAW/WAR 可以

| | RAW | WAW / WAR |
|---|---|---|
| **根本原因** | **数据流**：consumer 的输入 = producer 的输出 | **名字冲突**：共享寄存器名，彼此不传数据 |
| **物理本质** | 计算图上的有向边——值必须从生产者传到消费者 | 同一个变量被复用了——但每次都算的不同的值 |
| **如果强行消除** | consumer 拿到错误数据 → **程序语义崩溃** | 改名字后语义完全不变 → **纯粹是优化** |
| **硬件代价** | 旁路 MUX（ALU→ALU）+ tag 广播匹配（保留站）+ store buffer 地址比对（LSU） | 重命名引擎：RAT 查表 + 空闲列表 + 物理寄存器池 |

> **一句话**：RAW 是真依赖线（dataflow edge），不得不断；WAW 和 WAR 是寄存器复用造成的假名字冲突，换名字就行——这也是为什么现代 CPU 把 16 个架构寄存器映射到 180+ 个物理寄存器上。但 store/load 地址上的 WAW 和 WAR（同一内存地址的读写冲突）重命名管不了，靠 store buffer 按序提交 + memory disambiguation 保障。

## 六、前推的边界：不是所有 RAW 都能前推

| 前推无法解决的情况 | 为什么 | 延迟 | 应对 |
|---|---|---|---|
| **ALU→ALU** | ✅ 可以前推 | 1 拍 | 前推 MUX 旁路 |
| **Load→Use（L1 hit）** | 数据来自 D-Cache/MEM 级，不在 EX 级间寄存器里 | 5 拍 | 保留站 tag 监控，乱序填缝隙 |
| **Load→Use（L1 miss→L2）** | L2 访问 ~12 拍 | ~17 拍 | 保留站释放槽位，其他 µop 填 |
| **Store→Load（forwarding 命中）** | 数据从 store buffer 来，地址比对成功后直接给 | 0 额外（同 L1 hit 5 拍） | LSU store buffer 地址比对 |
| **Store→Load（forwarding 失效）** | 部分重叠 / 不对齐 / store 未就绪 | ~10+ 拍 | load 等 store 提交到 L1 D-Cache |
| **长延迟指令→Use（FMA/除法）** | FP 流水线深度 4~20+ 拍 | 4~20+ 拍 | 保留站 tag 监控，乱序绕开 |
| **跨集群依赖** | INT µop 的结果给 FP µop 用 | +1~2 拍走线延迟 | 跨集群旁路网络 |

> **保留站不是只做"乱序发射"——它是 RAW 依赖的硬件监控器**。每条 µop 携带源 PR tag，保留站逐拍监听结果总线上广播，命中则唤醒。长延迟的 RAW（cache miss、FMA、除法）全部由保留站管理——依赖 µop 在槽位里安静等待，调度器不断发射其他不依赖的 µop 填执行单元。

### 6.1 L1 D-Cache miss 场景下 RAW 的连锁反应

这是最值得理解的真实现象——一条 load miss 的连锁反应（循环 `for (i=0; i<N; i++) sum += a[i];`）：

```plantuml
@startuml
title L1 D-Cache miss：load 到 L2 的 14 拍窗口 & 乱序执行怎样填满它

participant "RS\n(保留站)" as RS
participant "AGU\nPort 2/3" as AGU
participant "TLB" as TLB
participant "SB\n(Store Buffer)" as SB
participant "L1\nD-Cache" as L1
participant "L2" as L2
participant "PRF" as PRF

== 拍 N~N+1：发射 & 地址阶段 ==
RS -> AGU: 发射 load µop LD a[i]
activate AGU
AGU -> TLB: VA → PA
activate TLB
TLB --> AGU: PA 返回 (1 拍)
deactivate TLB
AGU -> SB: PA 查 SB 全相联比对
activate SB
SB --> AGU: <color:#666666>未命中</color>
deactivate SB
note right #FFF3E0: TLB + SB 两路并发，1 拍内完成

== 拍 N+2：L1 查询 → MISS ==
AGU -> L1: PA 索引 + tag 比较
activate L1
L1 --> AGU: <color:#EF5350><b>L1 MISS</b></color>\n发起 L2 请求
deactivate AGU
note right #FFCDD2: 🔴 L1 tag 比较失败\n开始查下级 cache

== 拍 N+5：L2 收到请求 ==
L1 -> L2: L1 fill buffer → L2 请求 (PA)
activate L2
note right #E3F2FD: L2 查询 (~12 拍)\n此期间 RS 里依赖 P45 的 µop 全部等待

== 拍 N+14：L2 数据返回 → WB ==
L2 --> L1: <color:#FF9800><b>L2 数据返回 (+12 拍)</b></color>
L1 --> AGU: 数据锁存
deactivate L2
deactivate L1
AGU -> PRF: WB：数据写入 P45\n结果总线广播 tag=P45
activate PRF
deactivate PRF
note right #C8E6C9: ✅ P45 就绪\ndependent µop 被唤醒

note over RS, PRF #E8F5E9
  <b>★ 这 14 拍的等待窗口里，乱序核在做什么？</b>

  <b>① 发射不依赖 P45 的 µop</b>
  下一次迭代的 AGU 算 a[i+1] 地址——已经可以在拍 N+3 发射
  控制流 µop (CMP i,N / JNE) ——只要分支预测正确，继续取指

  <b>② 跨循环边界取指/译码</b>
  分支预测器提前判断下一次循环 → IF/ID 继续灌入新 µop
  ROB 里 a[i+1], a[i+2]... 的 load µop 排队进入 RS

  <b>③ 多条 miss 同时飞行 (MLP)</b>
  load buffer 支持多个 outstanding miss
  a[i], a[i+1], a[i+2] 的 L2 请求可以并行——硬件级 MLP
end note

@enduml
```

上述序列图揭示了 L1 miss 场景下 RAW 依赖的四个关键阶段，每个阶段都有其独特的硬件行为和对性能的影响。

**阶段一：地址生成并发（拍 N~N+1）——TLB 和 SB 为何能 1 拍并发？**

TLB 查询的是 VA→PA 映射，Store Buffer 比对的是 PA。按直觉，必须先拿到 PA 才能查 SB——但实际上这两路可以**并发启动**：AGU 发出 VA 后，VA 的 cache line offset 部分就是 PA 的 cache line offset（因为页内地址不变），SB 的 tag 比较电路可以直接用这部分做预匹配。TLB 返回完整 PA 后，最终确认即可。

这个 1 拍并发有两个硬件前提：
- **SB 是全相联的**：不需要先算 set index，直接对所有条目并行比 tag。典型 SB 深度 40~60 条，全相联硬件成本可接受。
- **页内 offset 共享**：VA[11:0] == PA[11:0]，SB tag 比较时这部分已经可用了。

> 为什么必须先查 SB？因为 SB 里可能有**尚未写回 L1 的 store 数据**——同一个地址的最新鲜版本在 SB 而不在 L1 D-Cache。如果跳过 SB 直接读 L1，拿到的是过期数据。

**阶段二：L1 D-Cache 访问与 tag 比较（拍 N+2）——MISS 的含义**

AGU 拿到 PA 后，用 set index 选组、tag 比较确定是否命中。现代 x86 L1 D-Cache 典型参数：32 KB, 8-way, 64B line，索引位 PA[11:6]。1 拍内并行读出 8 路 cache line tag，与 PA tag 做比较。

**L1 MISS 的两种可能**：

| 原因 | 说明 | 典型场景 |
|------|------|---------|
| **冷 miss** | 数据从未被加载过，cache 中无对应 line | 首次遍历大数组 |
| **容量/冲突 miss** | cache 已被其他数据 evict | 工作集 > 32 KB 的循环 |

无论哪种 miss，LSU 的 load buffer 会记录这条 miss 请求，分配一个 LFB（Line Fill Buffer，也叫 MSHR）。LFB 负责跟踪这条 miss 的 L2 请求状态。

**阶段三：L2 访问窗口（拍 N+5~N+14）——12 拍的等待里到底发生了什么**

从 L1 miss 到 L2 返回，物理时序拆解：

| 子阶段 | 拍数 | 硬件行为 |
|--------|------|---------|
| L1 → L2 请求传输 | 1 | LFB 向 L2 发起 PA 请求 |
| L2 tag 查询 | ~2 | L2 容量更大（256 KB~1 MB），set/way 更多 |
| L2 data array 读出 | ~6 | 数据线宽 × burst 长度决定 |
| L2 → L1 回传 | ~2 | fill buffer 写入 + ECC 校验 |
| **合计** | **~11** | — |

这 12 拍里，**依赖这条 load 的 µop 全部阻塞在 RS 中**——它们的源操作数 tag 指向 P45，而 P45 未就绪，调度器不会发射它们。

> **关键区别**：这不是 stall，而是**选择性暂停**。RS 里只有依赖 P45 的 µop 被阻塞，其他不依赖 P45 的 µop（包括同一循环体内其他独立的 load）照常发射。

**阶段四：WB & 唤醒（拍 N+14）——结果总线的广播机制**

L2 数据到达 L1 fill buffer 后，LSU 将数据写入 PRF 的第 45 号物理寄存器（P45），同时在**结果总线**（result bus，也叫 common data bus / CDB）上广播 `{tag=P45, data=val}`。

RS 中所有监听 P45 的 µop 在**同一拍内被唤醒**——不是轮询，而是硬件级的 tag 匹配电路，全并行。被唤醒的 µop 在下一拍（N+15）即可发射执行。

> 如果有多条 µop 同时依赖 P45（例如 `sum = a[i] + b[i] + c[i]`，`a[i]` 的 load 结果被两条 add 共用），它们会被**同一拍唤醒**，第二天同时发射。

**为什么 L1 miss 的 RAW 比 ALU→ALU 严重得多**

| 对比维度 | ALU→ALU RAW | Load→Use RAW (L1 miss) |
|---------|-------------|------------------------|
| 延迟 | 1 拍 | 14+ 拍 |
| 阻塞范围 | ≤1 条 µop | 所有依赖 P45 的 µop |
| RS 压力 | 几乎无 | 依赖 µop 占槽位 14 拍 |
| ROB 压力 | 几乎无 | 依赖 µop 不退休，挤占 ROB |
| 对 IPC 影响 | 微乎其微 | 依赖链长 → ILP 受限 → IPC 下降 |

**14 拍窗口中的三种乱序补偿机制**

序列图中的 note 已列出三条——这里逐一展开其硬件基础：

1. **发射不依赖 P45 的 µop**：RS 里每个槽位都有独立的 tag 匹配电路，调度器从所有"操作数已就绪"的 µop 中选最老的发射。P45 未就绪的 µop 不在候选池中，调度器自动绕过。

2. **跨循环边界取指/译码**：分支预测器在循环尾的 `JNE` 处预测"继续循环"，前端不断灌入 `a[i+1]`、`a[i+2]` 的 load µop。这些新的 load 不依赖 a[i] 的结果（地址独立），它们的 AGU µop 可以直接进入 RS——**MLP 的硬件基础**。

3. **多条 miss 同时飞行（MLP）**：LSU 的 load buffer 支持多个 outstanding miss（典型 10~12 个 LFB/MSHR）。`a[i]` 的 L2 miss 不阻塞 `a[i+1]` 的 L1 查询——如果 `a[i+1]` 也 miss，L1 可以立即再发一个 L2 请求。这是**硬件级的流水化 miss 处理**，不等前一条回来就先发下一条。

> **但 MLP 有上限**：LFB/MSHR 数量有限（~10 个），第 11 条 miss 不能再发 L2 请求，L1 必须 stall 等待前面的 fill 完成。对于顺序访问数组的情况（相邻 cache line 不冲突），10 个 LFB 足够覆盖 L2 延迟。对于随机访问，MLP 由 LFB 数量硬上限。

**极端情况：当 RAW 链长度超过 ROB 深度**

如果依赖链全是 load→use 且 miss 率高（如随机链表遍历），每条 miss 的 ~200 拍远超 ROB 深度（200~500 条目）。ROB head 处的 load 未完成，所有后续 µop 都不能退休——即使 RS 可以把它们发射出来（地址计算类的 µop 不依赖 load 结果），ROB 还是被堵死。一旦 ROB 满，前端取指停摆，整个流水线卡死在 head 处一个 load miss 上——IPC 骤降至接近 0。

> **如果依赖链全是 load→use 且 miss 率高**（如随机链表遍历），乱序执行也救不了——每条 miss 都是 200+ 拍，ROB 深度有限（200~500 条目），head 处一条 miss 堵住整个 ROB → 对头阻塞 → IPC 接近 0。

## 七、与微架构其他文档的关系

| 文档 | 本文与它的关系 |
|------|-------------|
| **[pipelining-superscalar.md](/concepts/microarch/pipelining-superscalar.md)** | 流水线是冒险产生的根源（指令阶段重叠），本文是冒险本身的 µop 级完整分析 |
| **[uop-pipeline-walkthrough.md](/concepts/microarch/uop-pipeline-walkthrough.md)** | 三条指令的逐拍 µop 走读——本文的各种冒险场景是其理论基础 |
| **[register-renaming.md](/concepts/microarch/register-renaming.md)** | WAW/WAR 假依赖的硬件消除机制（RAT + 物理寄存器池）——本文 §三、§四 的解法 |
| **[reservation-station.md](/concepts/microarch/reservation-station.md)** | RAW 真依赖如何通过 tag 监控在保留站里等待（操作数就绪才发射）——本文 §二的核心 |
| **[lsu.md](/concepts/microarch/lsu.md)** | store→load forwarding：特殊的 RAW（同一地址），走 store buffer 旁路——本文 §2.4；load→use 的 LSU 完整路径——本文 §2.3 |
| **[cpu-out-of-order.md](/concepts/microarch/cpu-out-of-order.md)** | 乱序执行全景：保留站+ROB+重命名如何协同管理三种冒险 |
| **[rob.md](/concepts/microarch/rob.md)** | ROB 保证 WAW 和 WAR 在退休时的顺序语义（旧 PR 在退休时才释放） |
| **[cache-organization.md](/concepts/cache/cache-organization.md)** | L1 D-Cache 的组织结构（set/way/tag）——本文 §2.3 load path 的底层硬件 |
| **[store-buffer.md](/concepts/memory-ordering/store-buffer.md)** | Store Buffer 在多核内存序中的角色——本文 §2.4 的 store→load RAW 是其单核侧 |

## 八、一句话总结

> **RAW（先写后读）是真依赖——consumer 的输入就是 producer 的输出，不可消除。ALU→ALU RAW 靠前推旁路压到 1 拍；Load→Use RAW 走 AGU→TLB→Store Buffer 查找→L1 D-Cache 全路径，至少 5 拍（L1 hit）；Store→Load RAW 走 store buffer address matching + forwarding，命中即零额外延迟，但部分重叠/不对齐则失效。WAW（先写后写）和 WAR（先读后写）是假依赖——寄存器级 WAW/WAR 靠重命名分配不同 PR 完全消除；Store WAW 靠 store buffer 按 ROB 序提交到 L1 D-Cache 保障；Load→Store WAR 靠 memory disambiguation predictor 处理。保留站不是只做乱序发射——它是 RAW 依赖的硬件监控器，tag 广播匹配机制让依赖 µop 在槽位静待生产者完成，调度器在等待期间持续发射其他不依赖的 µop 填充执行单元。长延迟 RAW（cache miss/FMA/除法）在乱序机器里的代价不是 stall 插气泡，而是限制 ILP——依赖链长度决定 IPC 上限。**

