多周期执行:怎样在几拍之间保存一条指令

一条 lw 要取指、读基址、计算地址、读取内存,最后写回寄存器。若这些动作必须在一拍里结束,时钟周期就要覆盖整条路径。把它拆成几拍可以缩短每拍必须完成的组合路径,也允许同一硬件部件在不同拍承担不同任务。代价是指令要占用更多周期,控制器还必须保存中间结果。

中心问题是:拆开的每一拍究竟保存什么,下一拍怎样知道继续做哪一步?本篇用第 07 篇相同的 .hex 求和程序,构建实际推进状态的多周期解释器。证据等级包括“教学时序模型”和带明确题设的“手算”。模型没有门级网表、综合时钟或面积数据,因此不能据此断言某块硬件变快或变小。

保存状态以后,组合逻辑才能跨拍复用

单周期通路中的某个中间值在该拍内一直可见。进入多周期设计后,组合逻辑输入可能在下一拍改变;后续仍要使用的值必须写进寄存器。指令字需要 IR,寄存器堆的读出值需要 A/B,地址或算术结果需要 ALUOut,读取内存的结果需要 MDR。实际实现可以合并或重新命名这些寄存器,所保存的信息不能凭空消失。

Berkeley CS152 的多周期数据通路讲义展示了这些跨周期寄存器及控制状态。讲义使用 MIPS;这里借用的是多周期组织方法,指令语义来自本系列的 RV32I 子集,具体状态数由本篇模型决定。

模型中的 Packet 保存指令、两个操作数、结果、有效地址和下一 PC。multicycle.py 的循环每次只处理一个状态:IF 读取指令字,ID 读取寄存器,EX 运算,MEM 访问数据,WB 写回。它没有读取 C 执行轨迹再按指令名称加周期。C 程序只在事后提供独立的提交结果用于比对。

控制状态决定当前允许改变的对象

本模型的正常执行路径是:

1
2
3
4
IF -> ID -> EX -- ALU ------> WB -> IF
|-- load --> MEM -> WB -> IF
|-- store -> MEM --------> IF
+-- branch --------------> IF

分支在 EX 决定下一 PC。store 在 MEM 写数据,随后取下一条指令;load 在 MEM 读出的数据还要经过 WB 才成为目标寄存器的新值。表里的 PC 更新指当前指令完成时接受 next_pc,并不是每一拍都把 PC 加四。

状态与指令类别 组合动作 本拍结束后保留的值 允许的架构写入
IF,所有指令 以 PC 读指令字 IR、原始 PC
ID,所有指令 译码,读 rs1/rs2 A、B、立即数与控制类别
EX,ALU A 与 B/立即数运算 算术结果、next_pc
EX,load/store A 加符号扩展偏移 有效地址、store 数据
EX,branch 比较操作数、选择后继 PC 分支结果 PC
MEM,load 以有效地址读取数据 读出值
MEM,store 以有效地址写入 B 完成标志 内存、PC
WB,ALU/load 选择算术结果或读出值 完成标志 rd(排除 x0)、PC

IRWrite、A/BWrite、ALUOutWrite、MDRWrite、MemWrite、RegWrite 可以由这张表逐行推导。例如 MEM-load 允许 MDRWrite,禁止 MemWrite 和 RegWrite;下一拍 WB 才允许 RegWrite。控制器不能只输出“下一状态”,也要输出当前状态的写使能。

这里的控制表是一组教学约束,没有为所有加法器指定物理共享关系。主 ALU 可在不同指令中承担算术或地址运算;PC 加四、分支目标加法和比较是否共用 ALU,会影响状态划分。若把这些动作也强制放到同一块 ALU 上,就需要重新检查某一状态是否要求它同时计算两个结果,不能保留原状态数却忽略资源冲突。

同一个求和程序得到 116 个模型周期

实验沿用基址 128、五个输入 3,4,5,6,7,把和写入地址 148。动态执行包含 18 条 ALU 指令、5 条 load、1 条 store 和 5 条分支。指令存储器和数据存储器在本实验中都被当作一次访问一拍。

类别 动态条数 每条周期 总周期
ALU 18 4 72
load 5 5 25
store 1 4 4
branch 5 3 15
合计 29 平均 4 116

逐拍输出见 sum-multicycle.txt,控制表见 control-states.txt。每行都有周期、动态指令编号、PC 和状态,可以逐条加总,而不必相信正文里的汇总数字。

最终提交记录包含 x3=25mem148=25。验证程序对每条指令的 PC、指令字、操作、目标寄存器或内存事件、next_pc 做完整 JSON 对象比较;执行错误和最终 halt 对象也参加比较。C 与 Python 没有共享译码或算术实现,两个执行器读同一输入文件。

下载 pipeline-lab.zip 后,在解压目录运行:

1
2
3
4
python3 examples/computer-architecture/pipeline/src/verify.py
python3 examples/computer-architecture/pipeline/src/run.py \
examples/computer-architecture/base/programs/sum.hex \
--multicycle --cycles /tmp/sum-multicycle.jsonl

第一条命令重新编译 C 参考并运行整个本批回归;第二条只执行多周期求和。需要 Python 3.10+ 与 C11 编译器,没有第三方 Python 包。完整记录见 verification.json:其中 41 个程序在多周期模型中与参考一致,另有三份非法输入在解析阶段被拒绝。执行错误案例也算一次差分,不能把这些数字称为全部正常结束的程序。

周期更短,不保证总时间更少

取一个明确的课堂题设:单周期设计的周期为 800 ps,多周期设计为 220 ps。这两个数字只用于算式,没有来自任何器件或综合报告。在该题设下,相同的 29 条动态指令有:

1
2
单周期:29 × 800 ps = 23.20 ns
多周期:116 × 220 ps = 25.52 ns

多周期每拍短了很多,总时间仍多出 10%。它的平均 CPI 是 4,周期缩短倍数只有约 3.64。只有时钟缩短足以抵消更多周期,时间才会减少。复用硬件也可能带来面积或功耗方面的收益,但这些收益需要另一组测量证据。

实际周期至少要覆盖当前最慢状态的组合路径,加上寄存器的 clk-to-q、setup 以及所采用的时钟裕量。若增加选择器或让一块 ALU承担更多输入路径,最慢状态也可能变长。先规定状态数,再任意给一个极短时钟,会把物理约束从比较里删掉。

内存未完成时,状态要能够保持

--memory-cycles 4 把每次数据访问的 MEM 占用设为四拍。求和有六次数据访问,原来各占一拍,现在各多三拍,因此总周期应增加 18,得到 134。这是可复跑的模型预测;附带的延迟实验还覆盖了按地址返回不同占用时间的接口。

MEM 等待期间不能重复写 store,也不能提前让 load 进入 WB。模型只在最后一个 MEM 周期执行数据读写,等待拍保留地址与数据。这里用一个整数表示总占用拍数;真实设备可能有请求、接收、返回及重试等不同握手阶段,本模型没有这些接口时序。

练习与验收

把求和的多周期参数改为 --memory-cycles 4,检查状态日志是否有 134 行,并检查最后的 mem148 是否仍为 25。答案来自六次数据访问各增加三拍,不是所有 29 条指令都增加三拍。

再考虑把分支比较与目标地址计算放到唯一的一块 ALU,且两者不能在同一拍完成。应增加或重新安排分支状态,并重新计算分支 CPI;不能只把硬件图中的两个部件画成一个而沿用三拍分支。此题没有唯一微架构答案,验收点是能够指出冲突和需要保留的中间值。

本篇验收要求能从 lwsw 和分支分别画出状态路径,指出每一拍允许的写入,并用 IC × CPI × 周期长度 比较题设中的执行时间。完成脚本只证明教学状态机及所测输入通过,RTL 综合、实际频率、面积和功耗仍未验证。

阅读衔接

前一篇:计算机体系结构 07:有限 RV32I 参考执行器

参考资料