← 返回笔记列表

这两天,我重新理解了一遍 Tomasulo

从消除模块调用顺序依赖,到比较三种分支预测器:一次关于状态、时序与可解释性的实现记录。

2 分钟阅读

这两天的大部分时间都在完善一个 C++17 的 RV32I Tomasulo CPU 模拟器。最初的问题看起来只是“模块应该以什么顺序调用”,但继续追问后才发现,真正需要解决的是:如何让软件实现忠实表达同一时钟周期里的硬件状态变化。

如果一个模块直接读取另一个模块刚刚写入的 next state,那么 C++ 函数的先后顺序就会变成隐藏的组合逻辑。模拟器可能通过测试,却无法说明交换两个模块的调用顺序后为什么仍然正确。

最后采用的状态协议是:

current → plan → apply → next → latch
  • 所有模块只观察 current state;
  • plan 先生成并冻结本周期的决策与信号;
  • apply 只修改各自的 next state;
  • latch 在周期末统一让 next 成为新的 current。

为了验证这条边界,CPU 会随机改变模块的 Plan、Apply 和 Latch 顺序。如果不同随机种子仍得到相同的架构状态,才说明实现没有偷偷依赖宿主语言的执行顺序。

另一条线是分支预测。我保留了 Bimodal 基线,实现 GShare,并进一步组合局部历史与全局历史形成 Tournament Predictor。在项目全部数据的 41,041,824 次条件分支上,三者准确率分别为:

PredictorAccuracy
Bimodal82.52%
GShare84.23%
Tournament86.36%

Tournament 相比 Bimodal 提升 3.83 个百分点,减少 1,572,659 次误预测。更重要的收获不是这个数字本身,而是理解了预测时保存索引、Issue 时投机更新历史、Commit 时训练、错误预测时按 checkpoint 恢复之间的关系。

这次实现也提醒我:把代码“写出来”和把系统“想明白”是两件不同的事。测试负责告诉我哪里错了;架构图、问答和 benchmark 则迫使我解释它为什么应该这样工作。

完整实现与数据流图见 RISC-V Tomasulo CPU Simulator。