仿真结果怎样可信

一个模型输出“程序结果为 25,耗时 100 周期”,包含两类不同的断言。结果为 25 需要检查指令语义;100 周期需要检查阶段、端口、队列和延迟假设。即使两者都符合模型定义,仍不能直接推出某台真实处理器也会耗时 100 周期。

本篇的核心问题是:怎样把功能正确、模型内部一致和硬件拟合分开验收?已执行的核心案例是同一批 RV32I 程序的独立解释器差分,并通过故意破坏 x0 规则验证检查器确实能发现错误。统计口径另用第 13 篇事件记录手算。gem5 部分提供指定版本的待运行配置,状态为 NOT_RUN,没有本系列的 gem5 仿真结果。

实验目录为 examples/computer-architecture/ooo/。可下载 差分结果统计口径记录运行环境Python 功能解释器。这些文件对应功能执行或手算证据,不是硬件测量。

三个验证问题分别需要什么证据

功能验证关心给定输入是否产生规定的架构状态变化。对第 07 篇的有限执行器,可以逐条比较 PC、目标寄存器和值、内存地址和值、分支方向,以及正常结束或拒绝原因。

时序验证关心程序在声明的资源和事件规则下怎样推进。第 12 篇需要验证消费者不能早于生产者完成而发射;第 13 篇需要验证年轻完成不等于年轻提交;第 14 篇需要验证未知地址解决后能触发重放。它们可以在功能结果相同的前提下有不同的时间轨迹。

硬件校准需要指定真实机器、输入、编译结果和可比测量区间,再判断模型预测与观测的差异。未测量就没有这一证据;把模型调到某一个输出数字相同,也不足以证明对其他输入仍然准确。

证据等级 可以支持的结论 不能直接支持的结论
手算 给定条件下的公式或事件计数 隐藏资源不存在
功能执行 已覆盖输入的语义结果 周期数正确
教学时序模型 指定资源与事件规则下的时间行为 商品处理器行为
gem5 仿真 指定 gem5 配置和工作负载的输出 自动等同目标硬件
真机测量 指定环境和测量区间内的观测 无条件推广到所有机器

证据等级不是模型名称的装饰。一个普通 Python 脚本只打印预先算好的数字,仍然只是手算附件;一个事件模型真正调度、等待和恢复,才能支持相应教学时序行为。gem5 配置文件尚未执行时,连“gem5 仿真”这一格也不能填入。

让两个实现执行同一份输入

本次差分把第 07 篇的 C 解释器作为参考,另写一个 Python 解释器。两者读取同一份 .hex 程序,独立译码、计算符号扩展、更新寄存器与内存,再生成相同契约的 JSONL 事件。

C 源码重新编译到 ooo/build/,不覆盖 base 的构建产物。Python 解释器没有导入 C 执行器或流水线的译码函数;这减少了直接共享同一函数造成的共同错误,但不保证两个实现不可能犯相同的规范理解错误。差分还需要手算、明确输入和拒绝边界配合。

比较对象包括全部提交事件以及结尾记录。例如一次 load 应同时匹配 PC、机器字、地址、目标寄存器、读取值和 next_pc。只比较最终求和结果可能漏掉临时寄存器错误、错误路径 store、不同的动态指令序列或偶然抵消。

对错误输入,事件比较也包括 error 的位置和原因。一个模型在错误指令处拒绝,另一个先当作 nop 执行一段再拒绝,不能视为相同。文件的畸形文本解析属于额外输入边界,本次 Python 解释器只接受已知教学格式,没有覆盖 C 参考执行器全部文本诊断。

九个程序的实际结果

实际运行使用 Python 3.14.4 与 macOS arm64 上的 C 编译器;完整命令和参考源码 SHA-256 保存在环境记录,程序 SHA-256 保存在差分记录。两种宿主语言执行的是模拟状态转换,不是在宿主 CPU 上直接执行 RV32I 机器码。

输入 对照内容 JSONL 事件数 比较结果
sum 五个数求和、循环、load/store 30 相等
branch_loop 分支循环求 1+2+3+4 17 相等
x0 硬零寄存器写入被丢弃 3 相等
signed_compare 负数与正数的有符号分支 5 相等
fail_unsupported 未支持的 slli 1 相等
fail_misaligned 非对齐 word load 1 相等
fail_oob 越过教学内存范围 1 相等
fail_store_text 写代码区被拒绝 1 相等
fail_branch_misaligned 分支目标不满足教学对齐规则 1 相等

总计 60 个事件,其中四个正常结束记录和五个错误记录不属于提交指令。正常程序共提交 51 条动态指令。sum 的 30 行由 29 条提交和一条 halt 组成,不能用文件行数直接作指令分子。

这九个程序证明这些输入下的完整轨迹一致。它们没有覆盖所声明子集中所有操作数、全部分支结果和所有支持指令组合,更没有覆盖完整 RV32I、原子扩展或特权体系。参考实现本身也可能有缺陷;“参考”表示本次比较中的角色,不是 ISA 合规认证。

故意破坏 x0,验证检查器的敏感度

故障版本仅允许 rd=0 的写入生效。x0 程序的第一条指令尝试把 123 写到 x0,第二条再以 x0 为源计算 x1。正确模型和故障模型第一个事件就出现差异:

1
2
3
正确:rd=0, value=0,   x0=0
故障:rd=0, value=123, x0=123
首次差异事件下标:0

检查器实际记录了这个反例,回归要求它必须被发现。若删掉整个事件比较,只保留“两个进程都正常退出”,这个故障仍可能被错误接受。

故障注入只能证明检查器对这一类错误敏感。它不等于把错误实现修正,也不证明所有可能缺陷都能被九个程序发现。可继续加入有符号边界、最近生产者选择、错误路径副作用等有针对性的反例,避免仅重复大量相似的加法输入。

第 13 篇的提前 store 故障检查提供了另一种敏感度:trap 和 squash 控制事件可以全部出现,内存仍可能已经被污染。因此,提交轨迹、最终状态与恢复事件各有作用,不能只保留最容易比较的一项。

统计分子和时间区间要同时固定

第 13 篇的正确 ROB 轨迹里,C、B、D、A 都产生了 complete 事件,只有 A 产生 commit。取模型整数时刻 0 到 7 对应的八个周期区间,即 [0,8),得到:

1
2
3
4
完成条目数 = 4
提交条目数 = 1
完成条目 / 周期 = 4 / 8 = 0.5
提交条目 / 周期 = 1 / 8 = 0.125

这份记录是手算核对,由脚本从保存的 ROB 事件中计数。分子 4 包含异常条目与后来取消的年轻条目,不能把 0.5 标成该模型的提交 IPC。它也不是 RISC-V 指令计数,因为 ROB 案例操作是独立教学条目。

区间端点同样重要。第 12 篇把 t=0 发射到 t=6 结果可用记作六个时间单位;第 13 篇此处按时钟槽统计 0、1、…、7 共八个槽。两种口径各自清楚即可,但拿两篇的分子分母拼成一个“系列 IPC”没有意义。

gem5 的统计文档说明输出可能包含多个统计 dump,并分别列出模拟时间与宿主执行耗时。阅读实际 stats.txt 时还要确认统计项单位、reset 与 dump 区间、指令还是微操作。两次包含累计值的 dump 直接相加会重复计数;hostSeconds 缩短表示模拟器在宿主上运行得更快,不能据此判定模拟 CPU 加速。

gem5 模型选择回答什么问题

SimpleCPU 官方文档区分 AtomicSimpleCPU 与 TimingSimpleCPU:前者用 atomic memory access 的延迟估计,后者发送 timing 请求并等待存储系统响应。这里的 atomic 是模拟器访问接口类型,不能因为名字相同就理解为程序中的 C 原子操作或 RISC-V AMO。

若问题涉及缓存排队、端口回压或更具体的乱序行为,应选用能表达那些机制的 CPU 与内存模型。模型名称不能替代参数检查,使用 O3 类模型也不会自动变成某款商业乱序 CPU。

本文待运行脚本选择一个 X86TimingSimpleCPU、无缓存和一个 DRAM 通道,只用于建立“配置、输入、运行记录和统计文件能够对齐”的最小流程。它不复现 12–14 篇的重命名、ROB 和 LSQ 教学轨迹,也不把 x86 工作负载的数值与 RV32I 程序的动态指令数混为同一个实验。

冻结目标版本与待运行配置

待运行目标固定为 gem5 v25.0.0.1,发布页对应完整提交 ddd4ae35adb0a3df1f1ba11e9a973a5c2f8c2944。这只表示本实验选择的版本,不声称它是当前最新版本。

gem5-config.json.txt 保存参数清单,se_timing.py.txt 保存待运行脚本,workload.c.txt 保存输入源码。

参数 选定值或契约
运行模式 SE,仅用户态工作负载的系统调用模拟
处理器 单核 X86TimingSimpleCPU,1 GHz
缓存 无,CPU 端口接系统总线
内存 单通道 DDR3_1600_8x8,512 MiB 地址范围
二进制 Linux x86-64 静态 ELF,LP64;尚未生成
输入 3、4、5、6、7,输出应为 sum=25
预热
统计范围 instantiate 后 reset,到整个进程退出
退出检查 正常退出、返回码 0、输出匹配

脚本依据官方简单配置教程中的低层 SimObject 接口编写。本轮固定提交下的源码页面未成功获取,因此没有确认模板与该提交的实际导入兼容性;它只通过 Python 语法解析,没有在 gem5 中 import 或运行。配置清单明确保留这一缺口。

环境记录中的 gem5_path=null 表示 PATH 搜索没有找到 gem5gem5.opt,不证明磁盘上任何位置都没有该工具。本篇没有安装、编译或执行 gem5,也没有 config.ini 或 stats.txt 可以作为输出证据。

输入二进制与统计区间不能省略

第 07 篇的 .hex 是教学执行器约定的文本文件;gem5 SE 工作负载需要适配目标 ISA 和操作系统 ABI 的可执行文件。本机 macOS 编译出的 Mach-O 不能当作这里的 Linux 静态 ELF 使用。

在具备 Linux x86-64 静态链接工具链的环境中,待执行步骤是编译 workload.c,保存编译器版本、命令、二进制类型和 SHA-256,再使用固定提交构建的 gem5 运行。示意命令中的路径必须替换为真实文件:

1
2
3
4
cc -std=c11 -O2 -static workload.c -o workload
file workload
sha256sum workload
build/X86/gem5.opt --outdir=/absolute/run /absolute/se_timing.py /absolute/workload

这些 gem5 步骤尚未执行,不把预期的 sum=25 当作已观察输出。运行完成后,还应读取实际 config.ini,核对默认参数是否与问题相符;源脚本中没写的参数也可能影响行为。参数完整展开是选择模型之外的另一道检查。

这个待运行 workload 包含 C 运行时启动与打印。即使未来获得整进程 IPC,也不能把它直接称为“五次求和循环 IPC”。若研究循环本身,需要定义 ROI(Region of Interest,关注区间),明确怎样进入、退出和 reset/dump;不能从全部进程统计中猜一个比例扣除启动开销。

本篇验收与未验证边界

已执行命令:

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

实际 PASS 包括九个完整 JSONL 差分、x0 故障模型被发现,以及 12–14 篇有限模型回归。源程序、参考 C 源码散列和结果文件可互相核对。Python 语法解析、命令行帮助与非法参数退出也已检查;环境缺少 basedpyright,未进行该工具的静态类型检查。

gem5 二进制、目标 ELF 与散列、模板导入兼容性、真实 config.ini/stats.txt、重复运行以及硬件校准均未验证。博客页面是否构建成功是内容交付检查,不会补足这些技术实验缺口。

准备写下的判断 最小检查 本篇状态
九个输入语义一致 完整提交/结束/错误事件差分 已执行
检查器能发现 x0 破坏 故障注入并定位首差异 已执行
统计分子区间无混淆 从具体事件重新计数 已手算核对
选定 gem5 配置能够运行 固定版本实际执行并保存输出 NOT_RUN
能预测目标硬件 同输入同区间的硬件校准 未验证

两道练习

练习一: 两个模型的 sum 都输出 25,但一个执行了错误路径 store,之后又覆盖回正确值。最终结果差分能否发现问题?应增加什么比较?

解答:仅比较 sum 未必发现。需要比较按序架构提交事件、内存写入与异常/分支恢复边界;若写入可能被外部观察,还需要相应观察模型。最终内存恢复成相同值,不自动撤销已经发生的外部副作用。

练习二: 同一 gem5 输出里有两个相同的统计块,各自记录 100 条指令与 50 个周期。可否把结果写成“完成 200 条指令”?如果第二次运行 hostSeconds 减半,模拟 CPU 是否快了一倍?

解答:先检查两块的 reset 和 dump 关系;若是同一区间的重复 dump,只能算一次。hostSeconds 是宿主执行模拟器耗时;判断模拟程序耗时应检查同一区间的模拟时间或周期,并保持模型和输入一致。当前文章没有实际 gem5 统计,此题是口径推导。

参考资料