ROB 与精确异常

重命名允许年轻指令提前计算,也允许多个尚未提交的寄存器版本同时存在。程序仍需要一种可解释的异常状态:若某条指令失败,异常处理程序看到的状态应对应顺序执行到该指令之前,而不能是几条年轻指令已经生效、几条较老指令尚未完成的混合结果。

本篇的核心问题是:执行顺序被打乱以后,怎样让异常边界仍然对应一段完整的程序前缀?案例在长延迟操作之后注入异常,并让更年轻的 store 最先完成。验收同时检查寄存器和内存,尤其检查提前完成的 store 没有污染架构状态。

证据等级为教学时序模型。四个条目的完成时刻是题设;模型运行后产生完成、提交和异常事件。它没有真实访存延迟、页表、RISC-V 陷阱 CSR 或异常处理程序。原始记录见 rob.json.txt,可独立执行的附件见 rob_model.py.txt

架构状态需要对应一个程序前缀

设程序顺序为 A、B、C、D。A 需要很久,B 最终报告异常,C 是写内存的操作,D 写寄存器。异常处理时允许 A 已生效,不允许 B 自己的正常结果、C 的 store 或 D 的寄存器写入生效。

“精确”描述的是这个边界。它不表示异常一被发现就立即进入处理程序,也不表示异常期间硬件内部没有任何推测活动。异常可以早于较老指令完成而被检测到,但体系结构可见状态仍要等到合适的提交位置。

BOOM 的 ROB 文档区分了完成与提交,并规定队首异常触发流水线清除,年轻指令的架构变化不成为可见结果。本篇用更小的条目模型展示这个条件;完成时刻和提交带宽并非 BOOM 的实测参数。

ROB 保存程序顺序

ROB 是 Reorder Buffer,即重排序缓冲区。名称中的“重排序”容易让人只想到把结果重新排列,实际上它至少需要保存指令的程序先后、是否完成、是否异常,以及提交或恢复所需的信息。

调度器可以先发射 C,再发射 B;ROB 的队列顺序仍然是 A、B、C、D。完成反馈只更新对应条目的状态。提交逻辑从队首检查:未完成就等待;完成且正常则提交;遇到队首异常就恢复到该位置对应的架构边界。不能因为 D 已经 ready 就跨过 B 提交 D。

本模型的条目保存寄存器结果或 store 的地址和值,类似把结果保留到提交时再应用的简化设计。真实处理器也可以把值保存在物理寄存器文件里,让 ROB 主要保存状态和恢复信息。BOOM 的 Rename Stage 文档明确对比了显式物理寄存器设计和 data-in-ROB 设计;“有 ROB”不等于“所有执行结果都放在 ROB 内”。

1
2
3
4
5
6
程序顺序   A → B → C → D

ROB [A][B][C][D] 队首只能从左向右推进
完成反馈 ↑ ↑ ↑ ↑ 完成次序可以不同

架构状态 仅应用已获准提交的前缀

第 12 篇的物理映射说明了旧值怎样继续存在,本篇的提交边界说明了哪个版本能够成为正式架构状态。两者结合以后,年轻指令提前完成不会迫使架构状态提前变化。

长延迟之后注入异常

初始状态设为 r1=0、r2=0、mem[128]=7。四个条目按下表进入 ROB:

条目 语义或事件 完成/异常已知时刻
A_long r1 = 42 t=6
B_fault 产生同步异常 t=2
C_store mem[128] = 99 t=1
D_add r2 = 88 t=3

条目已经在 t=0 前入队,模型每个整数时刻先接收完成反馈,再尝试一次队首提交。提交宽度固定为一;完成和提交允许在同一时刻发生。t=1 的 store 完成只表示地址和值可供提交,不能据此更新内存。

实际输出中,t=1、2、3 分别记录 C_store、B_fault、D_add 完成。t=2 已经知道 B 异常,但 A 还没有完成,因而 t=2 不能把 A 跳过。到 t=6,A 完成并提交 r1=42;同一时刻提交额度已用完。t=7 队首变成 B,触发异常并清除 C、D。

时刻 完成反馈 提交或恢复 架构状态变化
1 C_store ready 队首 A 未完成
2 B_fault 异常已知 队首 A 未完成
3 D_add ready 队首 A 未完成
6 A_long ready 提交 A_long r1=42
7 处理 B_fault,取消 C、D

最终记录为:

1
2
3
4
{
"registers": [["r1", 42], ["r2", 0]],
"memory": [[128, 7]]
}

C 和 D 虽然都完成了,r2 仍为 0,地址 128 仍为 7。这里没有通过“先写入再把值改回去”恢复内存;它们的架构副作用从未被提交。

用顺序前缀检查恢复结果

仅检查最终 r1=42 不够。一个坏实现也可能保留 r1,却漏掉年轻 store 的写入。顺序参考应同时覆盖所有被模型表示的架构状态。

附件中的 reference_prefix 按程序顺序应用条目,遇到第一个 fault 立即停止。A 被应用,B 和后面的条目不被应用,因此参考状态恰好是上面的 r1、r2 和内存。ROB 运行结束后对整个状态比较,而不是只挑一个结果寄存器。

这个参考函数与乱序时刻无关。把 C 的完成时刻从 t=1 改成 t=5,正确的异常状态仍然相同;把 A 的完成时刻推迟,则异常处理时间可能推迟。时间变化与语义变化应分别观察。

验收还包含一个无异常的对照:A 在 t=3 完成,年轻 store S 在 t=1 完成。A 于 t=3 提交,S 于 t=4 提交,此时内存才变成 99。这样可以排除“模型从来不提交 store,所以异常测试通过”的错误实现。

提前写 store 的故障反例

故障模式 early_store=True 只修改一条规则:store 一完成就立即更新内存。其他 ROB 顺序和异常处理逻辑保持不变。于是 t=1 出现:

1
{"cycle": 1, "event": "unsafe_memory_write", "name": "C_store"}

t=7 仍然出现 trap,C 和 D 也仍然被列为 squashed。单看“取消列表正确”会误判为恢复成功,但最终 mem[128]=99,与顺序参考的 7 不同,matches_reference 为 false。

这说明检查恢复不能只检查控制信号,还必须检查可能已经传播出去的副作用。普通寄存器结果可以保留在未提交版本里;内存写若已经被其他观察者看见,再把值写回原值也未必能撤销先前影响。本篇只模拟单线程内存状态,尚未加入其他核或设备,因此没有测量这种外部观察。

store 提交与写入内存仍有距离

本教学模型把“store 获准提交”和“更新内存数组”放在同一个动作里。这样可以用十几个事件解释异常隔离,但它省略了真实存储系统的队列和可见性。

BOOM 文档中的 store 在提交后获得写内存许可,由 LSU 在后续时机排空。提交、store buffer 排空、缓存一致性完成,以及其他核何时观察到结果,不应被压成同一个时间点。第 14 篇只讨论单线程的 load/store 依赖;跨核顺序还需要后面的内存模型章节。

同样,本篇的异常条目是人为注入的状态,不把它称为真机 page fault。若要实现真正的 RISC-V 异常,还需要依据特权规范保存异常 PC、原因、相关地址,并建立跳转和返回语义。当前模型的 trap 只是轨迹事件。

恢复时要清除哪些状态

有限模型把寄存器结果保留在条目里,所以丢弃年轻条目就能阻止年轻寄存器结果生效。使用物理寄存器文件的实现还要恢复映射并处理年轻分配的位置;分支预测历史、未完成请求和执行单元中的条目也可能需要标记或取消。

这些结构能否恢复应由各自的不变量验收。例如年轻 load 即使不能提交,发出的缓存访问也可能留下微架构痕迹;本模型没有缓存状态,不能由“架构状态正确”推出“所有内部痕迹都被清空”。侧信道选修将单独讨论这种边界。

对于本文的条目模型,验收条件可以直接写成:异常位置之前的所有正常条目按序生效;异常条目及其后条目不产生架构写入;最终寄存器和内存与顺序前缀一致。这些条件比“ROB 里没有残留”更贴近程序可见语义。

检查问题 对应记录 本例结果
较老 A 是否先完成并生效 complete、commit t=6 两事件
异常是否在队首处理 trap 的时刻与名字 t=7 的 B_fault
年轻寄存器是否泄漏 最终 r2 0
年轻 store 是否泄漏 最终 mem[128] 7
检查器能否发现已知错误 early_store 对照 状态比较失败

复跑与验收

在仓库根目录执行累计回归:

1
PYTHONDONTWRITEBYTECODE=1 python3 examples/computer-architecture/ooo/src/run_all.py

下载附件后可以独立执行:

1
python3 rob_model.py.txt

阅读 outputs/rob.json 中的 correct、broken_early_store 和 without_fault 三部分。正确模型必须与顺序前缀相等;故障模型必须不相等;无异常对照必须真正写入 99。三项合起来才覆盖这次实验的结论。

本模型没有实现分支检查点、ROB 容量回压、不同提交宽度、物理寄存器回收、设备访问或跨核可见性。该测试也不验证第 07 篇执行器的特权异常语义,因为那一执行器并未实现它们。

两道练习

练习一: B 的异常在 t=2 已知,为什么不立即跳到处理程序,并让 A 以后完成?

解答:若处理程序在 A 生效之前读取架构状态,会看见缺少一条较老指令的结果,不能对应异常前的顺序前缀。可以采用不同恢复机制,但必须补足这个状态条件。本模型通过等待 A 提交使条件成立;没有实现先回滚再重执行较老指令的其他方案。

练习二: 把提交宽度改为二,允许同一时刻依次检查两个队首条目,异常最早能否在 t=6 处理?年轻 store 能否一并提交?

解答:按照这个新题设,t=6 可以先提交 A,再看到作为新队首的 B 异常,因此异常处理可提前一周期。C 仍在 B 之后,必须被取消,不能因为还有带宽就越过异常提交。这个推导是手算,当前附件只执行宽度为一的配置。

参考资料