计算机体系结构 10:数据与结构冒险
数据与结构冒险:为什么正确的指令会读到错误的值
下面四条指令顺序执行时,x2 应为 14,x3 应为 21,地址 160 最后保存 21。把它们直接放进五级流水线,若没有前递和互锁,第二条却可能用初始的 x1=0 算出 x2=0。
1 | |
中心问题是:一个源寄存器在当前拍应从哪里获得数值,又在什么时候必须等待?本篇在第 09 篇的执行器中故意关闭保护,保存真实错误输出,再用前递与停顿恢复正确行为。全部周期与数据均属于“教学时序模型”,没有使用真机 PMU 或 gem5。
RAW 依赖需要确定最新的生产者
RAW 指先写后读的真实数据依赖。第一条 addi 在第三拍 EX 已经计算出 7,但要第五拍 WB 才写入 x1。第二条 add 在第三拍 ID 读 x1,读到的仍是旧值。它在第四拍 EX 使用这个旧值,就会产生错误结果。
CS61C 数据冒险材料分别讨论了停顿、同拍先写后读寄存器堆,以及向 EX 前递的路径。本模型采用 WB 先写、ID 后读的约定:距离较远的依赖可能因此直接读到新值;相邻依赖仍需要额外机制。
故障模式可直接运行:
1 | |
此参数同时关闭前递与 RAW 互锁,是用于观察错误的配置。保存的 raw-broken.txt 中,第二次提交的 value 为 0;raw-repaired.txt 中对应值为 14。这个故障不是手写“预期输出”,而是执行器真的读了旧操作数后产生的提交事件。
前递的选择顺序不能颠倒
当 EX 需要寄存器 r,前递逻辑先检查 EX/MEM 中较新的结果,再检查 MEM/WB 中较老的结果,最后才使用 ID 捕获的值。匹配条件还要排除 x0、无效包、不写寄存器的指令以及尚未产生数据的 load。
1 | |
例如连续两条指令都写 x1,后面的读者需要第二次写入。若 MEM/WB 匹配优先于 EX/MEM,就可能把第一次结果转发过去。回归中的 newest_writer 先写 1,再写 9,随后执行 add x3,x1,x1,要求提交 18;它专门区分这两个选择顺序。
x0 也不能被当作一般目标寄存器。addi x0,x0,9 的编码中 rd 确实为 0,但架构 x0 不会变成 9;后续读 x0 必须直接得到 0。对 lw x0,... 也不能制造一个“等待 x0 新值”的 RAW 停顿,不过这个 load 的访存本身仍然会执行和检查地址。
修复后的四条 raw_chain 指令共八拍,达到这个短序列的 N+4。若用 --interlocked 关闭前递、保留检测,则等待生产者完成 WB 后再放行,结果相同但共十四拍。前递节省了等待周期,并不改变原程序的依赖关系。
load-use 的值来得更晚
ALU 结果在 EX 末已经可用,load 的数据却要在 MEM 末才可用。对相邻的 lw x2,0(x1); add x3,x2,x2,load 的 MEM 与 add 的 EX 原本落在同一拍。按本模型的边沿和组合路径划分,不能把本拍末才读出的数据送回同拍已开始的 EX。
因此检测到 ID 的源寄存器依赖 EX 中的 load 时,模型保持 PC 与 ID 包,向下一个 EX 位置插入一个空槽。load 继续前进,下一拍它进入 MEM,消费者仍在 ID;再下一拍消费者进入 EX,从 MEM/WB 得到 load 数据。
| 周期 | ID | EX | MEM | WB | 动作 |
|---|---|---|---|---|---|
| 4 | add | lw | addi | — | 保持 add,插入气泡 |
| 5 | add | — | lw | addi | load 读取数据 |
| 6 | sw | add | — | lw | add 使用前递数据 |
完整记录见 load-use-stages.txt。该案例四条指令需要九拍,比 N+4 多一拍。这里的“一个气泡”依赖一拍 MEM 和这套前递路径;若缓存未命中让 MEM 占四拍,等待就不再只有一个周期。
store 同时有地址与数据依赖
sw x2,0(x1) 的 x1 参与有效地址计算,x2 是将要写入的数值。只对 ALU 的两个数值输入做表面匹配,容易漏掉 store 数据;S 型指令中的部分位属于立即数,也不能误当作 rd。
本模型在 EX 获得 store 的基址和数据,两者都参与前递与依赖检测。因而 lw x2,...; sw x2,... 同样需要一个 load-use 气泡。另一种数据通路可以增加 MEM 阶段的 store-data 旁路,让某些这种相关不必在 EX 等待;那是另一套时序约定,需要单独画清楚旁路终点。
store_sources 案例分别生成基址 160 和数值 37,再把 37 写到 160,随后 load 读回。load_store_data 则直接检测 load 的结果能否正确成为下一条 store 的数据。这些案例与 x0、最新生产者案例共同参加 C/Python 逐提交差分。
结构冒险来自同一拍的资源竞争
数据已经准备好也不代表能前进。如果指令取指和数据访存只能使用同一个端口,MEM 里的 load/store 就会占掉当拍 IF 的机会。这是资源冲突,添加寄存器前递路径不能制造第二个端口。
ports.hex 有七条指令。分离端口时十一拍,单端口时十三拍。单端口模式明确记录两次实际丢失的 IF 机会;程序末尾已经没有指令要取时,MEM 占端口不会再计一次虚假的 IF 停顿。对照记录见 ports-comparison.txt。
1 | |
这只是端口数量的教学对照,不等同于测量某种缓存的命中率、bank 冲突或真实带宽。后续缓存模型可以经 memory_latency 回调返回总 MEM 占用拍数,但多个并行 miss、MSHR 和独立请求队列不在当前接口中。
MEM 等待时还要保护已经进入流水线的操作数。较老 WB 可以在等待期间退出;被冻结的 EX 包必须接收并保留这次前递,不能等 MEM 解除等待以后再寻找一个已经消失的生产者。回归同时覆盖长延迟、单端口、预测和无前递互锁的组合,防止只在理想一拍内存中正确。
复跑与验收
下载 pipeline-lab.zip,执行 python3 examples/computer-architecture/pipeline/src/verify.py。记录 verification.json 保存 287 次五级差分、故障反例和计数检查;源码哈希、输入哈希和配置一起保留。模型支持本系列 13 条指令,完整 RV32I 与硬件时序没有被验证。
练习一:在 addi x1,x0,1; addi x1,x0,9; add x3,x1,x1 中故意把前递优先级反转,指出哪个阶段包含旧值、哪个阶段包含新值。验收答案必须定位首次错误的第三次提交,不能只说“结果不对”。
练习二:把 lw x2,128(x0) 改为 lw x0,128(x0),后接读取 x0 的 add。RAW 停顿应消失,数据访存仍存在;若使用单端口,其结构冲突仍可能保留。能够同时解释这两点,才能区分数据依赖与资源竞争。
本篇验收要求先复现错误,再说明前递来源和优先级,指出 load-use 在何处等待,并追踪 store 的两个源。只有最终总和正确,不足以通过该项验收。
阅读衔接
前一篇:计算机体系结构 09:五级流水线。
参考资料
- CS61C Data Hazards:写后读、旁路与 load-use 的教学路径。
- CS61C Structural Hazards:资源竞争与端口组织。
- RISC-V RV32I,Version 2.1:x0、load/store 操作数和指令语义。
- 附件
pipeline/src/core.py与逐事件输出:本文的实际错误、修复和周期计数证据。






