📑 目录
摘要
1. 🏗️ 设计目标与总体架构
1.1 🎯 目标设定
1.2 🧩 顶层结构
1.3 🖼️ 模块连接框图
1.4 🕸️ 模块实例化关系(概览)
2. 📜 指令集与编码
2.1 📋 指令集设计
2.2 🔢 编码格式
2.3 🧮 辅助函数
3. 🌊 五级流水微架构
3.1 🎬 阶段划分
3.2 📦 流水线寄存器
3.3 🌿 分支指令的处理
3.4 💦 分支冲刷与 slot1 的处置
3.5 📊 流水线占用表
3.6 🌊 流水阶段流转(时序视角)
4. 🚀 双发射判据与冒险处理
4.1 ✅ 双发射基本约束
4.2 🏷️ 判据的分类统计
4.3 📥 load-use 的处理
4.4 ⏸️ halt_pending 机制
4.5 🌳 双发决策树
4.6 🕸️ 模块关系图
5. 🔀 前递网络设计
5.1 📊 前递源与优先级
5.2 🎯 前递的基准值
5.3 ⏱️ 前递的时序成本
5.4 ⏱️ 信号交互时序图
5.5 🔀 前递路径流程图
6. 🗄️ 寄存器堆与 write-through
6.1 📊 端口配置
6.2 💡 write-through 的必要性
6.3 ⚖️ write-through 的代价
6.4 🔄 写回与读的时序对照
7. 🔮 分支预测
7.1 🧱 结构
7.2 🎯 预测规则
7.3 🔄 更新规则
7.4 🔗 与发射逻辑的耦合
7.5 🧊 冷启动行为
7.6 🔄 BHT 状态机
8. 🧪 统计与验证方法学
8.1 🎯 RTL 与统计的职责划分
8.2 📋 观察口清单
8.3 🎯 attempt 的统计口径
8.4 🧪 测试程序集
8.5 📊 统计流程图
9. 🔄 迭代历程与关键修正
9.1 🥚 第一轮:非对称双发原型
9.2 📤 第二轮:统计迁移到 C++
9.3 🧩 第三轮:冒险与前递独立化
9.4 🎯 第四轮:统计口径修正
9.5 ⚡ 第五轮:三项改进叠加
9.6 🔮 第六轮:分支预测
9.7 💡 第七轮:write-through 修复
9.8 🔄 迭代时间线
10. 📈 实验数据与分析
10.1 📊 总体结果
10.2 🔍 逐项观察
10.3 📊 IPC 对比可视化
11. ⚖️ 设计权衡与已知局限
11.1 🎯 取舍回顾
11.2 🚧 已知局限
11.3 🚀 后续工作方向
12. 🎉 结论
📌 摘要
本文记录一款五级流水顺序双发射 RISC 处理器的完整设计演进过程。
设计从最小可运行的非对称双发射原型出发,依次完成统计口径迁移、冒险单元独立化、执行通道对称化、Load 前递、NOP 支持、动态分支预测以及寄存器堆 write-through 修复。
每个阶段都配有可复现的周期与 IPC 数据,用以量化单项改进的边际收益。
最终实现以 8 组测试程序覆盖独立指令流、依赖链、混合负载、load-use、分支、NOP 填充、load 前递和循环八类典型场景,并在双发模式下取得 1.00×–1.44× 的 IPC 加速区间 🚀
1. 🏗️ 设计目标与总体架构
1.1 🎯 目标设定
本设计面向教学与微架构研究场景,主要目标如下。
-
• 🧱 建立一条完整的五级顺序流水线,覆盖 IF、ID、EX、MEM、WB 五个阶段 -
• ⚡ 支持每周期最多两条指令的发射,发射宽度为 2 -
• 🔍 提供可观测的冒险分类统计,用于量化每类阻塞的代价 -
• 🧪 保持 RTL 与统计逻辑分离,统计口径由 testbench 决定 -
• 🎛️ 允许逐项启用改进,便于对照实验
非目标包括乱序执行、多线程、缓存层次和虚拟内存。
💭 这些特性会显著增加验证复杂度
与"可解释性优先"的设计取向冲突
因此被有意排除在本次迭代之外
后续若扩展缓存与异常,会另立实验分支
1.2 🧩 顶层结构
顶层模块为 core,内部实例化以下子模块。
|
|
|
|
|---|---|---|
branch_predictor |
|
|
hazard_unit |
|
|
regfile |
|
|
forwarding_unit |
|
|
alu
|
|
|
core
|
|
|
cpu_pkg 作为类型包存在,不在硬件层次中占据节点,仅通过 import 被各模块引用。
🎯 分工原则
每个模块只做一件事
冒险判定在hazard_unit
前递选择在forwarding_unit
预测查表在branch_predictor
顶层core只负责连线和流水寄存器推进
1.3 🖼️ 模块连接框图
该图给出综合后的层次视图。读图可关注以下节点。
-
• 📦 core是唯一顶层实例,其余五个模块均为其子模块 -
• 📚 cpu_pkg不占据硬件节点,仅以import形式被各模块引用 -
• 🔁 regfile读写口之间的回环即 write-through 路径 -
• ⚙️ 两个 alu实例分别对应 slot0 与 slot1 的执行通道 -
• 🔮 branch_predictor跨在 IF 与 EX 之间,一端接 PC,一端接 EX 分支结果 -
• 🚦 hazard_unit输出仅数根控制线,但决定 PC 增量是 +4 还是 +8 -
• 🔀 forwarding_unit有四个源操作数与四个输出,是全设计最宽的组合路径 -
• ⏰ 时钟树与复位树不显式画出,隐式存在于所有 always_ff中
📖 读图要点
数据通路是粗箭头链regfile → fw → alu → dmem → regfile
控制通路是hazard_unit与branch_predictor输出的细箭头
观察口以圆角标注,仅用于 C++ testbench 采样
图中的层次与 RTL 模块一一对应
1.4 🕸️ 模块实例化关系(概览)
除原理图外,也可以用简化的流程图表示模块之间的实例化关系。
-
• 🟦 core是唯一顶层 -
• 🟩 其余模块均以 u_xxx前缀实例化 -
• 🟨 cpu_pkg是编译期依赖,与运行期连线无关 -
• 🟫 regfile与dmem是存储器类模块,与组合逻辑区别对待
🎯 为什么用
u_前缀
与模块名区分,便于在综合报告里快速定位实例
与 Verilog 的层级名保持一致,波形中可直接搜索
也是多数项目采用的约定
2. 📜 指令集与编码
2.1 📋 指令集设计
指令集为单周期定长的极简 RISC 子集,共 11 条指令。
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
🎯 为什么是这 11 条
覆盖 ALU 类、访存类、控制类三类指令
NOP 用于验证流水线填充与双发合法性
LI 用于构造测试循环的初值
组合起来足以构造所有需要的冒险场景
后续若需要乘除法,可在此表基础上增补
-
• 🧮 ALU 类:ADD、SUB、AND、OR、XOR、LI -
• 💾 访存类:LW、SW -
• 🚦 控制类:BEQ、HALT -
• ⚪ 空操作:NOP
2.2 🔢 编码格式
指令字长 32 位,字段布局如下。
-
• 📏 [3:0]:opcode -
• 📏 [8:4]:rd -
• 📏 [13:9]:rs1 -
• 📏 [18:14]:rs2 -
• 📏 [31:19]:13 位有符号立即数
立即数采用符号扩展,LI 指令复用该字段作为源操作数。
🎯 定长编码的取舍
定长便于译码:opcode 位置固定
定长便于 PC 递增:每条指令 4 字节
缺点是立即数字段窄:13 位有符号
但本设计的程序规模远小于 4K 指令,够用
若需要更大立即数,可借用 rs2 字段
2.3 🧮 辅助函数
cpu_pkg 提供若干 pure function,用于统一各模块对指令属性的判断。
-
• decode(w):从 32 位字解码为instr_t -
• is_alu(op):判断是否为 ALU 类指令(含 LI) -
• is_load_store(op):判断是否为访存指令 -
• is_supported_slot1(op):判断 slot1 通道是否可接受该指令 -
• writes_rd(op):判断是否写目的寄存器
🛡️ 统一判定的价值
hazard、forwarding、core 三处都需要判断"这条指令写不写 rd"
若各处自己写一份case,后续加新指令容易漏改
集中在cpu_pkg后,加新指令只需改一处
这也是 SystemVerilogpackage的标准用法
3. 🌊 五级流水微架构
3.1 🎬 阶段划分
|
|
|
|
|---|---|---|
|
|
|
i0
i1、can_dual
|
|
|
|
rs1_d0/rs2_d0/... |
|
|
|
opa/opb
actual_taken
|
|
|
|
ex_mem_dmem_* |
|
|
|
wd0/wd1 |
⚡ 单周期能完成什么
IF 每周期取最多两条指令
EX 每周期并行算两条
这是本设计"发射宽度 2"的物理体现
从 IF 到 WB 共 5 个周期,对应 5 个流水级
3.2 📦 流水线寄存器
四组流水寄存器构成级间状态。
-
• if_id[0:1]:包含instr、pc、valid、pred_taken -
• id_ex[0:1]:包含op、a、b、rd、rs1、rs2、imm、pc、valid、is_load、pred_taken -
• ex_mem[0:1]:包含op、result、rd、valid、is_load -
• mem_wb[0:1]:包含data、rd、valid、we
每组带独立 valid 位,用于在冲刷时精确清空。
pred_taken 字段从 IF 一路传递到 EX,供分支解析阶段与实际结果比对。
🧷 valid 位的作用
分支冲刷时只清valid,不必逐字段清零
双发模式下[0]与[1]可分别控制
一条被冲刷、另一条继续,是双发流水的基本粒度
有效位也用于done的判定,确保流水排空
3.3 🌿 分支指令的处理
分支解析放在 EX 阶段,主要出于以下考虑。
-
• ✅ EX 阶段前递已就绪, opa0、opb0即为真实比较值 -
• ❌ 若前移至 ID,需额外的比较前递路径,且与 BP 的预测点冲突 -
• ⚖️ EX 解析的代价是两个周期的错误预测窗口,与 BP 的收益折中后仍为净收益
🎯 为什么不在 ID 解析
ID 阶段寄存器值尚未经过前递选择
若在 ID 比较,需要额外的比较前递路径
这会使 BP 查询点与 EX 更新点错位
EX 解析的 2 周期延迟可以用预测器覆盖
3.4 💦 分支冲刷与 slot1 的处置
predict_miss 发生时,id_ex_v 整体清零,if_id_v 整体清零。
若预测错误的方向是"预测不跳、实际跳",flush_slot1 同时置位,避免错误路径上的 slot1 指令进入 ex_mem。
若预测错误方向为"预测跳、实际不跳",flush_slot1 不置位,因为该情形下 slot1 本就无效。
🎯 slot1 单独的处置
双发时两条指令可能来自不同路径
slot0 的 BEQ 已经确定跳转
slot1 的顺序下一条已经作废
只冲刷 slot1 可保留 slot0 的正确结果
3.5 📊 流水线占用表
|
|
|
|
|
|
|
|
|
|
|---|---|---|---|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
图例
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
粗体 |
|
|
|
|
|
斜体 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
以 P8 循环体为例,观察点如下。
-
• 🟥 c0 列两格,@16 与 @20 同周期取指,是双发的起点 -
• 🟦 c1 列三格,上一对进入 ID,新一条进入 IF,稳态三层重叠 -
• 🟩 c2–c4 列均为五格,每周期有五条指令散落于不同级 -
• 🟧 c5–c7 逐列递减,对应流水线 drain 阶段 -
• 📈 8 周期跑完 5 条指令,IPC 0.625,双发收益被五级深度摊销
同一列出现两个非空单元格 = 该周期双发(c1 有 @16/@20 的 ID,c2 有 @16/@20 的 EX 等)。
每周期并发事件可紧凑表述如下。
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3.6 🌊 流水阶段流转(时序视角)
用状态图也可以表达"同一周期里 5 条指令各处于哪个阶段"。
-
• 🎯 状态图对应单条指令的流转 -
• 双发相当于同一周期有两条指令分别进入不同状态 -
• 每个状态都有对应的一组流水寄存器承载状态
4. 🚀 双发射判据与冒险处理
4.1 ✅ 双发射基本约束
对称双发成立需要同时满足以下条件。
-
• ✔️ slot0 与 slot1 都有效 -
• ✔️ slot0 不是 HALT -
• ✔️ slot0 不是 BEQ 或 BP 未预测 taken -
• ✔️ slot1 不是 BEQ,也不是 HALT -
• ✔️ slot1 的指令类型在 is_supported_slot1允许集合内 -
• ✔️ slot1 的 rs1、rs2 不依赖 slot0 的 rd -
• ✔️ halt_pending未置位
🎯 为什么 slot1 不能是 BEQ
本设计只有一个分支单元
两条分支同周期会争抢 BP 更新与 PC 选择
硬件上不划算,软件上也极少出现连续分支
若未来支持双分支单元,可放宽此约束
4.2 🏷️ 判据的分类统计
hazard_unit 将失败原因分类输出,便于量化每类阻塞的占比。
|
|
|
|
|---|---|---|
|
|
rs1/rs2 == rd
rd != 0
|
block_raw |
|
|
!is_supported_slot1 |
block_slot1 |
|
|
!predict_taken
|
|
|
|
halt_pending |
|
block_raw 在多数程序中占比最高,说明相邻指令的写后读是双发的主要损耗来源。
💡 分类统计的意义
若只看 IPC,无法判断瓶颈来自 RAW 还是 slot1 类型
分开统计后,可以定位"应该优先优化哪个方向"
这也是后续改进的依据所在
更细致的分类需要加 WAW / WAR 统计
4.3 📥 load-use 的处理
早期版本在 ID 阶段检测 load-use,命中时插入一拍 bubble。
引入 Load 前递后,该检测被移除,load_use 恒为 0,但保留接口以维持兼容性。
这一改动同时影响单发与双发模式,因为 load-use 停顿与发射宽度无关。
4.4 ⏸️ halt_pending 机制
HALT 到达 EX 之前,IF 会继续取指,取出 HALT 之后的垃圾指令。
早期实现中,这些垃圾指令会触发 dual_attempt 与 block_slot1,造成统计偏差。
修正方案引入 halt_pending 寄存器。
-
• 🟢 IF 看到 HALT 时置位 -
• 🔴 HALT 到达 EX 时清零 -
• 💦 分支冲刷时清零
该寄存器接入 dual_attempt 的条件判断,使垃圾指令不再进入尝试计数。
🎯 为什么要清零于分支冲刷
若 HALT 之后的路径被 taken 分支跳过
它其实永远不会执行
若不清零,后续所有双发都会被错误禁止
清零时机与ex_halt的检测点要对齐
4.5 🌳 双发决策树
从 IF 到 ID,双发要经过若干条件判定,可以用决策树表达。
-
• 🌳 从上到下逐级判定 -
• 🟥 红色节点为单发或阻塞 -
• 🟩 绿色节点为成功双发 -
• 🟦 蓝色为决策起点 -
• 每个失败分支都对应一个统计信号或"不计入 attempt"
4.6 🕸️ 模块关系图
该图以颜色与线型区分依赖性质,与本节的双发判据直接对应。
颜色与形状 🎨
-
• 🟨 菱形表示 cpu_pkg,package 而非硬件 -
• 🟦 双框表示 core,顶层 -
• 🟩 方框表示普通模块(bp、hu、fw、alu×2) -
• 🟫 圆柱表示存储器(imem、dmem、regfile) -
• 🟥 圆角表示观察口 obs_*
线型含义 📏
|
|
|
|
|---|---|---|
|
|
|
core ==> alu0 |
|
|
|
regfile --> forwarding_unit |
-.->
|
|
cpu_pkg -.-> core |
--x
|
|
BP --x hazard_unit |
三条与双发相关的路径 🔍
-
• 🎯 BP --x hazard_unit表示预测 taken 时禁止 slot1 发射 -
• 🚦 hazard_unit -> PC决定本周期取 4 字节还是 8 字节 -
• 🔀 forwarding_unit是唯一跨三级(EX/MEM/WB)的组合逻辑,是双发下最紧的时序点
📖 两条环路的区分
数据大环regfile → fw → alu → dmem → regfile决定关键路径
控制小环hu → PC → imem → hu决定每周期发射宽度
预测反馈alu → bp → PC决定分支处理效率
三者独立,互不干扰
5. 🔀 前递网络设计
5.1 📊 前递源与优先级
前递单元覆盖 EX、MEM、WB 三级,优先级按指令年龄从年轻到年长排列。
|
|
|
|
|---|---|---|
|
|
ex_mem[1] |
|
|
|
ex_mem[0] |
|
|
|
mem_wb[1] |
|
|
|
mem_wb[0] |
|
|
|
|
|
ex_mem 阶段 load 数据的前递依赖对 dmem 的组合读,路径为"地址计算 → dmem 读 → ALU"。
🎯 为什么年轻优先
双发时ex_mem[1]是上一周期 slot1 指令
它比ex_mem[0](slot0)更"晚"进入流水
若两者都写同一 rd,ex_mem[1]的值才是正确的
优先级必须按"谁更新"排序
5.2 🎯 前递的基准值
前递单元的 fallback 值早期取 if_id 的读口输出。
该取值在单发模式下正确,因为 ID 与 EX 之间隔了一个流水寄存器。
双发模式下,id_ex[0] 与 if_id[0] 描述的是不同指令,错位导致部分依赖读取到错误值。
修正方案把 fallback 改为 id_ex[0].a/b 与 id_ex[1].a/b,即 ID 阶段写入 id_ex 时锁存的操作数。
修正后 P8 循环中的 SUB r1, r1, r4 能正确递减,双发模式不再死循环 🎉
🐛 bug 的隐蔽性
单发模式下 ID 与 EX 隔一个流水寄存器if_id[0]与id_ex[0]描述同一条指令
双发后两者分离,错位才显现
这类 bug 只有在"提高并发度"后才会暴露
5.3 ⏱️ 前递的时序成本
前递单元是唯一跨三级的组合逻辑。
EX 阶段的关键路径为:
regfile 读 → 3 级前递 mux → ALU → 结果
双发使前递单元端口宽度翻倍,但路径深度不变。
真实处理器通常把前递拆成 early/late 两级,以缓解该路径的时序压力。本设计保留单级,以换取 RTL 简洁。
⚖️ 取舍
单级前递 RTL 简单,但关键路径长
双级前递时序好,但控制逻辑复杂
教学项目优先可读性,选择单级
后续若提高频率,需要重新评估
5.4 ⏱️ 信号交互时序图
该图以五个 rect 区块展示 c0–c4 的参与者与动作,直接刻画了前递在网络中的时间窗口。
逐周期要点 ⏱️
-
• 🔴 c0: IF → BP送if_pc;BP → IF返predict_taken与predicted_target;IF → HU送两条候选指令 -
• 🔵 c1: HU → IF回can_dual;RF → FW送出四条源操作数 -
• 🟢 c2: WB → FW前递参与选源;FW → A0/A1送opa/opb;A0 → BP反馈actual_taken更新 BHT/BTB -
• 🟠 c3: A0/A1 → WB锁存进ex_mem[0]与ex_mem[1];load 的 dmem 读出也在此完成 -
• 🟣 c4: WB → RF写回wd0/wd1;write-through 使同周期读口立即可见
线型对照 🧷
|
|
|
|
|---|---|---|
|
|
|
IF → BP: if_pc |
|
|
|
WB → FW: mem_wb_data |
|
|
|
IF → HU: i0/i1 |
|
|
|
WB → RF: wd0/wd1 |
图中两处为示意性简化,读图时需注意。
-
• ⚠️ actual_taken实际反馈给branch_predictor,不经hazard_unit -
• ⚠️ write-through 是 regfile 内部逻辑,不作为独立信号
📖 时序图的三层价值
单看 RTL 很难理解"每周期谁在用谁"
单看波形太琐碎,无法概括
时序图介于两者之间,是理解握手的关键
报告里适合与框图配套呈现
5.5 🔀 前递路径流程图
前递选择可以画成一条从寄存器堆到 ALU 的路径,用于强调"三级 mux"的概念。
-
• 🟫 前递源: regfile、mem_wb、ex_mem三类 -
• 🟦 两级 mux:年轻优先 -
• 🟩 ALU 入口:两条执行通道共享同一份前递结果 -
• 若改为两级前递,mux 级数会再增加
6. 🗄️ 寄存器堆与 write-through
6.1 📊 端口配置
regfile 提供 4 个读口与 2 个写口。
-
• 📥 读口: rs1_a0、rs2_a0、rs1_a1、rs2_a1 -
• 📤 写口: we0/rd0/wd0、we1/rd1/wd1
4 读口对应双发的四条源操作数,是双发射能成立的结构前提。
🎯 为什么是 4 读 2 写
每条指令最多两个源操作数
双发 = 每周期最多四条源
每条指令最多一个目的
双发 = 每周期最多两个目的
端口数量由发射宽度直接决定
6.2 💡 write-through 的必要性
单发模式下,ID 与 WB 相隔四个周期,同周期写后读不会发生。
双发模式使 ID 与 WB 可以同周期,同周期写后读的窗口被打开。
具体表现为:P8 循环中,上一轮 LI r1, 3 在 c4 写回,下一轮 SUB r1 在 c4 进入 ID 读口。
regfile 的数组更新发生在 posedge,组合读发生在 posedge 之前,导致读到旧值。
修正方案在读口加入同周期写可见逻辑。
-
• 🟢 若 we1且rd1命中,返回wd1 -
• 🔵 若 we0且rd0命中,返回wd0 -
• ⚪ 否则返回数组值
优先级为 slot1 > slot0 > 数组,与"年轻覆盖年长"的语义一致。
🐛 P8 超时的完整链路
LI r1, 3在 c4 写回SUB r1, r1, r4在 c4 读 r1
读到旧值 0 → 结果为 −1
r1 永远不减到 0@28分支永不 taken
循环永不退出 → 200000 周期超时
单发模式此窗口不重叠,所以正常
6.3 ⚖️ write-through 的代价
组合路径从"数组直出"变为"数组输出 + 2 级 mux"。
面积增加为每个读口 2 个 32 位 2:1 mux,共 8 个 mux,相对 regfile 本体可忽略。
时序增加约 0.1–0.2 ns,在综合后需评估 EX 入口路径的余量。
📌 为什么不上前递到 ID
前递到 ID 需要额外的比较逻辑
还要与 EX 前递共存,复杂度翻倍
write-through 只在 regfile 内部改一点
是更优雅的解决方案
6.4 🔄 写回与读的时序对照
用简单的时序图可以直观对比"有无 write-through"的差别。
-
• 🟩 有 write-through:读口组合返回正在写入的值 -
• 🟥 无 write-through:读到 posedge 之前的旧值 -
• 差别只在同周期发生写和读时出现
7. 🔮 分支预测
7.1 🧱 结构
预测器由 BHT 与 BTB 两部分组成。
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
索引取 pc[9:2],tag 取 pc[31:10]。
7.2 🎯 预测规则
预测成立需同时满足三条。
-
• ✔️ 当前取指为 BEQ -
• ✔️ BHT 对应项 MSB 为 1 -
• ✔️ BTB 命中且 tag 匹配
任一条件不满足即预测 not taken。
7.3 🔄 更新规则
BHT 与 BTB 均在 EX 阶段更新。
-
• 📈 BHT:每次分支解析都更新,方向 +1 或 −1,饱和于 00 与 11 -
• 🎯 BTB:仅在 actual_taken时填表
BTB 的目标完全由 pc + imm 决定,静态可确定,因此一次填表即可长期有效。
🎯 为什么 BTB 只在 taken 时填
not taken 时目标就是顺序 pc+4
不需要额外记录
taken 时才是"意外的"目标
只需为 taken 记录
7.4 🔗 与发射逻辑的耦合
预测 taken 时,slot0 为 BEQ,slot1 不允许发射。
原因是 slot1 的指令不位于跳转目标的路径上,双发无意义。
这一约束使分支指令成为双发机会成本的来源:每次预测 taken 都损失一个 slot1。
⚖️ 预测的"机会成本"
预测 taken 本意是减少流水线冲刷
但预测 taken 时 slot1 被浪费
两条路径各有得失
最终收益取决于程序的 taken 比例
7.5 🧊 冷启动行为
2-bit saturating counter 需要连续两次相同方向才能翻转 MSB。
对于低频 taken 的分支(如每 N 轮才 taken 一次),该结构无法学到 taken 状态。
P8 循环中,@28 分支每轮仅执行一次且只在最后一轮 taken,因此预测准确率停留在 0/3。
这一现象揭示了两级自适应预测器与 TAGE 类结构的必要性,属于后续工作范围。
🎯 2-bit counter 的固有缺陷
强 NT → 弱 NT → 弱 T → 强 T
四次翻转需要四次 taken 事件
对稀疏 taken 分支几乎不工作
这是简单预测器的天花板
7.6 🔄 BHT 状态机
2-bit saturating counter 的状态转换可以用状态图描述。
-
• 🎯 4 个状态:强 NT、弱 NT、强 T、弱 T -
• 📈 taken 向右转,not taken 向左转 -
• 🧊 从复位进入强 NT,需要两次 taken 才能翻到 MSB=1 -
• 这解释了 P8 冷启动时准确率低的根本原因
8. 🧪 统计与验证方法学
8.1 🎯 RTL 与统计的职责划分
RTL 只输出组合观察口,不做任何计数。
C++ testbench 在每周期 clk=0 时采样观察口,累加到 Stats 结构。
|
|
|
|---|---|
|
|
obs_* 组合信号
|
|
|
|
这一划分使统计口径可以随实验需求调整,而不改动 RTL。
🎯 为什么统计放 C++
RTL 计数器不可重入,每次实验需重新编译
C++ 侧改一行就能重新统计
也便于用std::map之类做复杂分类
是验证驱动设计的常见做法
8.2 📋 观察口清单
|
|
|
|---|---|
obs_pc |
|
obs_issue_n |
|
obs_retire_n |
|
obs_dual_attempt |
|
obs_block_raw |
|
obs_block_slot1 |
|
obs_branch_taken |
|
obs_branch_pred_ok |
|
obs_branch_pred_miss |
|
obs_predict_taken_if |
|
obs_btb_hit |
|
obs_load_use |
|
8.3 🎯 attempt 的统计口径
早期实现中,dual_attempt 在 IF 阶段组合计算,包含随后被分支冲刷的周期。
这导致 P5 的 attempts 比真实双发次数多 1 次。
修正方案在观察口加入过滤条件:dual_attempt && !predict_miss && !ex_halt。
修正后,attempts − dual_cycles 恰为被冲刷的投机失败次数,语义清晰。
🎯 "幻想 attempt"的定义
IF 阶段确实做了判定
但结果被冲刷
是否计入统计是一种选择
选择"计入" = 前端工作量
选择"不计" = 有效尝试
本设计选择后者
8.4 🧪 测试程序集
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
每程序跑单发与双发两种模式,结果并列输出。
🎯 8 组程序的覆盖逻辑
P1–P3 覆盖 ILP 连续谱
P4–P5 覆盖两类流水线冒险
P6–P7 覆盖两项新特性
P8 覆盖分支预测器
组合起来覆盖所有关键设计点
8.5 📊 统计流程图
统计从 RTL 观察口到报告,可以画成简单的数据流。
-
• 🟫 观察口为组合信号,C++ 每周期采样 -
• 🟦 累加到 Stats结构体 -
• 🟩 报告阶段做派生计算:IPC、加速比、准确率等 -
• RTL 与统计完全解耦
9. 🔄 迭代历程与关键修正
9.1 🥚 第一轮:非对称双发原型
初始版本 slot1 仅支持 ALU。
hazard 与 forwarding 逻辑内联在 core.sv 中。
统计计数器位于 RTL 内部,以 stat_* 端口输出。
9.2 📤 第二轮:统计迁移到 C++
删除 RTL 的 stat_* 计数器与 always_ff 统计块。
改为组合观察口,由 C++ 侧累加。
收益是统计口径可随实验调整,代价是观察口端口数量增加。
9.3 🧩 第三轮:冒险与前递独立化
hazard_unit 与 forwarding_unit 抽成独立模块。
core.sv 的行数下降约 40%,模块职责边界清晰。
9.4 🎯 第四轮:统计口径修正
修复两处偏差。
-
• 🐛 halt_pending抑制 HALT 后垃圾指令的 attempt -
• 🐛 分支指令自身正常 retire,修正 P5 的 retired 计数
修正后 P1 的 success rate 从 66.7% 升至 100%,P5 的 IPC 从 0.500 升至 0.600。
9.5 ⚡ 第五轮:三项改进叠加
对称双发、Load 前递、NOP 支持三项同时落地。
|
|
|
|
|
|---|---|---|---|
|
|
block_slot1
|
|
|
|
|
dual_cycles
|
|
|
|
|
load_use
|
|
|
|
|
Cycles (single)
|
|
|
|
|
dual_cycles
|
|
|
三项改进正交可叠加,P4 的加速比从 1.20× 升至 1.375%。
🎯 三项改进的正交性
对称双发改的是"slot1 能干什么"
Load 前递改的是"数据什么时候到"
NOP 支持改的是"空操作能不能双发"
三者互不影响,收益可叠加
9.6 🔮 第六轮:分支预测
引入 BHT + BTB,预测 taken 时允许跳过顺序取指。
EX 阶段更新 BHT 与 BTB,并与 IF 阶段查询构成闭环。
预测错误时冲刷 IF/ID,预测正确时仅冲刷 slot1。
9.7 💡 第七轮:write-through 修复
P8 双发出现死循环,根因为 regfile 同周期写后读的 RAW。
修正方案在 regfile 读口加入 write-through 逻辑。
修正后 P8 从 200000 周期回落到 24 周期,done 正确置位 🎉
9.8 🔄 迭代时间线
-
• 🥚 基础阶段:搭骨架 -
• 🧩 结构阶段:拆模块、修正口径 -
• 🚀 功能阶段:双发、前递、预测 -
• 💡 修复阶段:write-through 收官 -
• 七个阶段层层叠加,每轮都保留可复现数据
10. 📈 实验数据与分析
10.1 📊 总体结果
|
|
|
|
|
|
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
10.2 🔍 逐项观察
P1 接近理想上限 ✅
8 条独立 ALU 指令在 5 个发射周期内完成(4 次双发 + 1 次单发)。
dual_attempts = 4,success rate = 100%。
加速比 1.44× 低于 2×,差额来自五级流水的 fill/drain 固定开销。
🎯 1.44× 的构成
双发使发射周期从 9 减到 5
省了 4 个周期
但流水线深度不变
总周期 13→9,比例 1.44
若要接近 2×,需要更深的发射宽度 + 更短流水
P2 下界验证 ✅
7 对相邻 ADD 全部被 RAW 拦截,block_raw = 7。
双发模式不引入额外 penalty,IPC 与单发一致。
🎯 "无损"的价值
依赖链场景双发不加速
但也不减速
说明 hazard_unit 判定精确
没有误伤
P3 混合负载 ✅
dual_cycles = 3,block_raw = 2,success rate 60%。
成功与失败比与代码的依赖密度一致。
P4 双重改进 ✅
load_use = 0、block_slot1 = 0,单发与双发 Cycles 同时下降。
证明前递与发射宽度是两个正交维度。
P5 分支代价 ✅
branch_flush = 1,attempts − dual_cycles = 1 即投机失败的代价。
Branch-pred accuracy = 0% 属于单次执行的冷启动必然结果。
P6 NOP 填充 ✅
dual_cycles = 3,success rate = 100%。
NOP 作为合法 slot1 后可被自由填充,验证 is_supported_slot1 的扩展正确。
P7 Load 前递边界 ✅
load_use = 0 但 block_raw = 1。
(LW, ADD) 的同组 RAW 无法通过前递消除,因两条指令同周期读 regfile。
这一现象区分了"跨周期 load-use"与"同组 RAW"两类冒险。
🎯 两类冒险的本质区别
跨周期 load-use:数据晚到,前递可救
同组 RAW:两条指令同周期读,前递不可救
前者是时间问题,后者是结构问题
P8 循环与预测 ✅
Branches taken = 3,Pred correct = 2,Pred miss = 3。
40% 准确率与 2-bit BHT 的学习曲线一致:@28 每三轮才 taken 一次,BHT 无法积累到 10。
10.3 📊 IPC 对比可视化
-
• 📊 第一组为 single,第二组为 dual -
• P1 双发最接近 1.0,P2 两模式一致 -
• P5 分支程序 IPC 最低 -
• 双发相对单发的增益在各程序中不同
11. ⚖️ 设计权衡与已知局限
11.1 🎯 取舍回顾
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
11.2 🚧 已知局限
-
• 🔮 无分支预测的 tournament 结构,低频 taken 分支学习能力不足 -
• 🔀 前递为单级,关键路径在高频下可能成为瓶颈 -
• 🧱 同组 RAW 无法通过 slot 交换解决,P7 的 (LW, ADD)仍单发 -
• 💾 无缓存层次,dmem 与 imem 假设单周期访问 -
• 🚨 无异常与中断支持
11.3 🚀 后续工作方向
-
• 🎯 引入 BTFN 静态偏置,作为 BHT 冷启动初值 -
• 🔄 增加 slot 交换逻辑,缓解同组 RAW -
• ⏱️ 把前递拆成 early/late 两级,缓解 EX 路径 -
• ⚡ 引入三发射或四发射,观察复杂度的非线性增长 -
• 💾 加入 L1 指令与数据缓存,评估访存延迟对 IPC 的影响
12. 🎉 结论
本设计以五级顺序双发射为骨架,通过七个迭代阶段逐步扩展功能与统计能力。
关键结论如下。
-
• 🚀 双发射的稳态收益由指令级并行度决定,P1 的 1.44× 与 P2 的 1.00× 分别给出上下界 -
• 🧩 非对称双发的 slot1 约束是主要资源浪费,对称化后 P4/P5 的 block_slot1归零 -
• 📥 Load 前递与发射宽度正交,前者消除跨周期 load-use,后者扩展并行窗口 -
• 🔮 分支预测的收益取决于程序的重复性,单次执行程序的冷启动成本不可忽视 -
• 💡 寄存器堆的 write-through 是多发射架构的必要条件,单发模式不会暴露该问题 -
• 🧱 同组 RAW 是双发的结构性约束,无法通过前递消除,只能通过 slot 交换或更宽发射缓解
所有八组测试程序的单发与双发模式均正确执行,done 信号正常置位,统计口径自洽可解释。
工程具备可复现、可扩展、可对照实验的特征,可作为超标量微架构教学与研究的基线实现 🎓

