大数跨境

顺序双发射 RISC 微架构的设计演进与性能评估

顺序双发射 RISC 微架构的设计演进与性能评估 ai算法芯片与系统
2026-11-08
6
导读:这是一个五级流水顺序双发射 RISC CPU 的 SystemVerilog 实现,用于教学与微架构研究,量化双发、前递、分支预测和 write-through 的 IPC 收益与冒险代价。

 

📑 目录

摘要
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
BHT + BTB 动态预测
IF 查询、EX 更新
hazard_unit
双发判据与冒险检测
IF / ID 组合逻辑
regfile
4 读 2 写 + write-through
ID 读、WB 写
forwarding_unit
四路源操作数选择
EX 入口
alu
 × 2
双执行通道
EX
core
 本体
PC、流水寄存器、dmem、控制
全局

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 条指令。

助记符
编码
语义
写 rd
ADD
0
rd = rs1 + rs2
是
SUB
1
rd = rs1 − rs2
是
AND
2
rd = rs1 & rs2
是
OR
3
rd = rs1 | rs2
是
XOR
4
rd = rs1 ^ rs2
是
LW
5
rd = dmem[rs1+imm]
是
SW
6
dmem[rs1+imm] = rs2
否
BEQ
7
if rs1==rs2 pc += imm
否
HALT
8
停机
否
NOP
9
空操作
否
LI
10
rd = imm
是

🎯 为什么是这 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 后,加新指令只需改一处
这也是 SystemVerilog package 的标准用法


3. 🌊 五级流水微架构

3.1 🎬 阶段划分

阶段
主要动作
关键信号
IF
读 imem、查 BP、判双发
i0
、i1、can_dual
ID
读 regfile、写流水寄存器
rs1_d0/rs2_d0/...
EX
前递选源、双 ALU 计算、分支解析
opa/opb
、actual_taken
MEM
dmem 读写、load 数据前递
ex_mem_dmem_*
WB
写回 regfile
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 📊 流水线占用表

Instruction
c0
c1
c2
c3
c4
c5
c6
c7
@16 ADD r2,r2,r4
🔴 IF
🔵 ID
🟢 EX
🟠 MEM
🟣 ‖WB‖



@20 ADD r3,r3,r4
🔴 IF
🔵 ID
🟢 EX
🟠 MEM
🟣 ‖WB‖



@24 SUB r1,r1,r4

🔴 IF
🔵 ID
🟢 EX
🟠 MEM
🟣 ‖WB‖


@28 BEQ r1,r0


🔴 IF
🔵 ID
🟢 EX
🟠 MEM
🟣 ‖WB‖

@32 BEQ loop



🔴 IF
🔵 ID
🟢 EX
🟠 MEM
🟣 ‖WB‖

图例

阶段
符号
颜色
线型/边框
含义
IF
🔴 IF
红
粗体
取指
ID
🔵 ID
蓝
斜体
译码/读寄存器
EX
🟢 EX
绿
正常
执行
MEM
🟠 MEM
橙
正常
访存
WB
🟣 ‖WB‖
紫
‖双竖线‖
写回

以 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 等)。

每周期并发事件可紧凑表述如下。

Cycle
并发事件
c0
IF:@16 → IF:@20
c1
ID:@16 → ID:@20 → IF:@24
c2
EX:@16 → EX:@20 → ID:@24 → IF:@28
c3
MEM:@16 → MEM:@20 → EX:@24 → ID:@28 → IF:@32
c4
WB:@16 → WB@20 → MEM:@24 → EX:@28 → ID:@32
c5
WB:@24 → MEM:@28 → EX:@32
c6
WB:@28 → MEM:@32
c7
WB:@32

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 将失败原因分类输出,便于量化每类阻塞的占比。

失败原因
判据
统计信号
RAW
rs1/rs2 == rd
 且 rd != 0
block_raw
slot1 类型不支持
!is_supported_slot1 block_slot1
分支未预测 taken
slot0 是 BEQ 且 !predict_taken
不计入 attempt
HALT 在流水
halt_pending
不计入 attempt

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
-.->
 点线
import 或弱依赖
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 三级,优先级按指令年龄从年轻到年长排列。

优先级
来源
是否可前递 load 数据
1
ex_mem[1]
是(mem-to-ex)
2
ex_mem[0]
是(mem-to-ex)
3
mem_wb[1]
是
4
mem_wb[0]
是
5
regfile
—

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 两部分组成。

组件
规模
用途
BHT
256 × 2-bit saturating counter
预测方向
BTB
256 × {valid, tag[21:0], target[31:0]}
预测目标

索引取 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 结构。

层次
职责
RTL
驱动 obs_* 组合信号
C++
累加、分类、报告

这一划分使统计口径可以随实验需求调整,而不改动 RTL。

🎯 为什么统计放 C++
RTL 计数器不可重入,每次实验需重新编译
C++ 侧改一行就能重新统计
也便于用 std::map 之类做复杂分类
是验证驱动设计的常见做法

8.2 📋 观察口清单

信号
含义
obs_pc
当前 PC
obs_issue_n
本周期进入 ID/EX 的指令数(0/1/2)
obs_retire_n
本周期 WB 完成的指令数(0/1/2)
obs_dual_attempt
IF 判定为双发尝试且未被冲刷
obs_block_raw
尝试失败:RAW
obs_block_slot1
尝试失败:slot1 类型不支持
obs_branch_taken
本周期分支实际 taken
obs_branch_pred_ok
预测正确
obs_branch_pred_miss
预测错误
obs_predict_taken_if
IF 预测 taken
obs_btb_hit
BTB 命中
obs_load_use
load-use 停顿(恒 0)

8.3 🎯 attempt 的统计口径

早期实现中,dual_attempt 在 IF 阶段组合计算,包含随后被分支冲刷的周期。

这导致 P5 的 attempts 比真实双发次数多 1 次。

修正方案在观察口加入过滤条件:dual_attempt && !predict_miss && !ex_halt。

修正后,attempts − dual_cycles 恰为被冲刷的投机失败次数,语义清晰。

🎯 "幻想 attempt"的定义
IF 阶段确实做了判定
但结果被冲刷
是否计入统计是一种选择
选择"计入" = 前端工作量
选择"不计" = 有效尝试
本设计选择后者

8.4 🧪 测试程序集

程序
目的
关键覆盖点
P1
全独立 ALU
双发上限
P2
纯依赖链
RAW 阻塞
P3
混合依赖
部分双发
P4
Load-use
访存依赖
P5
分支跳转
分支冲刷
P6
NOP 填充
NOP 双发
P7
Load 前递
mem-to-ex
P8
BHT 循环
动态预测

每程序跑单发与双发两种模式,结果并列输出。

🎯 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 支持三项同时落地。

改进
验证指标
改进前
改进后
对称双发
P4 block_slot1
1
0
对称双发
P4 dual_cycles
2
3
Load 前递
P4 load_use
1
0
Load 前递
P4 Cycles (single)
12
11
NOP 支持
P6 dual_cycles
—
3

三项改进正交可叠加,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 📊 总体结果

程序
IPC single
IPC dual
加速比
关键瓶颈
P1
0.692
1.000
1.444×
无
P2
0.692
0.692
1.000×
RAW
P3
0.692
0.900
1.300×
部分 RAW
P4
0.636
0.875
1.375×
访存依赖
P5
0.500
0.600
1.200×
分支冲刷
P6
0.636
0.875
1.375×
无
P7
0.667
0.889
1.333×
同组 RAW
P8
0.655
0.792
1.208×
冷启动预测

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 🎯 取舍回顾

决策
收益
代价
EX 阶段解析分支
前递已就绪,逻辑简单
错误预测窗口两个周期
单级前递
RTL 简洁
EX 关键路径较长
BHT 256 项
面积小
学习能力有限
统计在 C++
口径灵活
端口数量增加
非对称到对称的演进
P4/P5 收益明显
hazard 判据扩展

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 信号正常置位,统计口径自洽可解释。

工程具备可复现、可扩展、可对照实验的特征,可作为超标量微架构教学与研究的基线实现 🎓

 


【声明】内容源于网络
0
0
ai算法芯片与系统
长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
内容 249
粉丝 0
ai算法芯片与系统 长期关注ai领域,算法,芯片,软件(系统,框架,编译器,算子库)等联合设计
总阅读7.7k
粉丝0
内容249