WAW / WAR:假依赖—— 换个名字就消解的两种冒险
本篇是数据冒险拆分的假依赖篇:讲透 WAW(写后写) 和 WAR(读后写)——它们和 RAW 的本质区别(名字冲突 vs 数据流)、两个域的区分(寄存器域靠重命名消除、内存域靠 store buffer/ROB 与 memory disambiguation 保障)、以及三种冒险的 µop 级对比总表。前提:µop 视角与 RAW 寄存器域(ALU→ALU 前推)见 RAW 篇,RAW 访存域(Load→Use/Store→Load)见 访存 RAW 篇;总纲见数据冒险。
一、WAW(Write After Write):写后写——有两种,寄存器 WAW 和内存 WAW
WAW 分两个完全不同的域:寄存器 WAW 发生在物理寄存器号上,重命名阶段直接消除;内存 WAW 发生在同一 cache line 地址上,靠 ROB + store buffer 提交顺序保证——重命名管不到,硬件机制也完全不同。下面分开讲。
1.1 寄存器 WAW:重命名直接消除(正确性问题 → 重命名后不存在)
两条 µop 写同一个架构寄存器,经过重命名之前会争用同一个物理寄存器号,但它们的值彼此独立:
架构寄存器视角:
① 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:
重命名后:
µ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 读到的是错误的旧结果。
重命名怎么解的? 核心动作只有两步:
给每个"写"分配一个新 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。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。寄存器 WAW 只要给每个写分配不同的目的 PR 就完全消除——这是重命名最直接的价值,也是寄存器 WAW 在正确性层面其实并不危险的原因:所有现代 x86 核心都有重命名,寄存器 WAW 退化成了 RAT 资源紧张时的性能问题(无空闲 PR 则 stall),而非正确性问题。
1.2 Store WAW:地址相同的 store→store,重命名管不到
这是内存域的 WAW——两条 store 写同一个内存地址,最终内存里必须是后一条的值。
mov [rsi], rax ; store₁:写 [rsi] ← rax
mov [rsi], rbx ; store₂:写 [rsi] ← rbx(同一地址)store₁ 和 store₂ 都写 [rsi],最终内存里必须是 store₂ 的值。
硬件怎么保证:
- store₂ 的 STA µop 在 AGU 阶段算地址时,store 队列里有更早的 store₁(同一 ROB 顺序但更早分配),LSU 检测到地址冲突。
- store₂ 必须等 store₁ 退休(commit 到 store buffer 可提交状态)后才能写自己的数据到 store buffer。
- 或者更常见的实现:允许并行写 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 写入。下面分开讲。
2.1 寄存器 WAR:重命名直接消除
前面 µop 读某个架构寄存器,后面 µop 写同一个架构寄存器。后写的不能在前读完成之前执行(否则前读拿不到新值而非旧值):
架构寄存器视角:
① 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 写入了错误数据。
2.2 寄存器 WAR 的解法:同样是重命名
重命名后:
µop₂(①): STD 源=P10 ; 读 P10(旧 rax 值)
µop₃(②): ADD P52, P10, P20 ; 写 P52(新 rax 值),不碰 P10
★ µop₂ 读 P10、µop₃ 写 P52 —— 完全不同的物理寄存器,没有冲突和寄存器 WAW 同理:有重命名的核心里,寄存器 WAR 在正确性层面不存在,只会在极端寄存器压力下成为性能瓶颈。
2.3 Load→Store WAR:内存域的 WAR,重命名管不到
这是内存域的 WAR——load 读内存地址、后面 store 写同一地址,按程序序 load 应该读到 store 写入前的旧值:
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 §3.4 的 memory disambiguation。
三、三种冒险对比总结(µop 级)

| 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 |
3.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 保障。
四、一句话总结
WAW/WAR 是假依赖——共享同一个名字,但彼此不传数据,所以换名字就能消除:寄存器域的 WAW/WAR 靠寄存器重命名(RAT 给每个写分配新 PR、始终指向最新版本)完全消除,代价只是 PR 池资源;内存域的 WAW/WAR 重命名管不到,分别靠 store buffer 按 ROB 序提交(Store WAW)和 memory disambiguation(Load→Store WAR)保障程序序。它们与 RAW 的对比核心只有一句:RAW 是"值必须到",WAW/WAR 是"名字别打架"——前者决定正确性下限,后者只影响性能上限。