biriscv_npc 性能分析
分支预测器性能统计
基本信息
当 branch_request_i 拉高:
- RAS 学习
如果 BTB 命中或者不命中: - btb_pc_q 更新:
btb_pc_q[btb_wr_entry_r] <= branch_source_i - btb_is_call_q 更新:
btb_is_call_q[btb_wr_entry_r]<= branch_is_call_i - btb_is_ret_q 更新:
btb_is_ret_q[btb_wr_entry_r] <= branch_is_ret_i - btb_is_jmp_q 更新:
btb_is_jmp_q[btb_wr_entry_r] <= branch_is_jmp_i
或者没命中,也会记录这些信息:
- btb_pc_q 更新:
btb_pc_q[btb_wr_alloc_w] <= branch_source_i - btb_is_call_q 更新
btb_target_q[btb_wr_alloc_w] <= branch_pc_i - btb_is_ret_q 更新:
btb_is_call_q[btb_wr_alloc_w]<= branch_is_call_i - btb_is_ret_q 更新:
btb_is_ret_q[btb_wr_alloc_w] <= branch_is_ret_i - btb_is_jmp_q 更新:
btb_is_jmp_q[btb_wr_alloc_w] <= branch_is_jmp_i
总之,只要 branch_request_i 拉高,BTB 中一定会记录一个 PC 的情况,这是为了学习。
注意,此时 BTB 中的 {pc:npc} 中只有 btb_pc_q 更新,btb_target_q 并没有更新。只有当 branch_is_taken_i 拉高的时候,它才会更新:
1 | |
其实就算不拉高,相当于也在更新了,因为默认npc = pc+8。只是更新没有意义,考虑到功耗,不用更新了。
当 branch_is_taken_i 或者 branch_is_not_taken_i 拉高:
- bht_sat_q 就开始饱和计数。
性能统计
为什么 branch_request_i 会拉高呢?
当前端的 issue 部件发现两个指令都不是期望的指令时,这时候就会发射一组信号来修正 npc 部件的 next_pc 生成,这样 refetch 的时候 npc 就会发出正确的 PC,从而 issue 部件拿到期望的指令:
1 | |
这意味着如果 branch_request_i 拉高,一定是分支预测错误(一定是分支指令,如果不是,就会+8,但是为了万无一失,这里需要 trace 验证),或者是方向,或者是目标;如果 branch_request_i 没有拉高,说明预测单元给出的 npc 是正确的,但不意味着此时做出了分支预测(这是必然的,不需要验证)。
因此,一个思路是:
- 判断 PC 对应的指令是分支指令(跳转、call、ret等)的次数 M;
- 统计 branch_request_i 拉高的次数 N。
分支预测正确率就是 ((N/M)*100) %。
问题
经过验证,branch_request_i 拉高,它的指令不一定是分支指令。因此结论为:如果 branch_request_i 拉高,一定是不期待的指令,但不一定是分支指令。
指令流的顺序
首先需要搞清楚 frontend 数据通路。
riscv_top 顶层架构图
这个顶层包含三个主要模块:icache(指令缓存)、dcache(数据缓存) 和riscv_core(CPU核心),对外暴露 axi 总线,对内分别连接 icache、dcache。
graph TB
subgraph riscv_top["riscv_top 顶层模块"]
Core["riscv_core<br/>CPU核心"]
ICache["icache<br/>指令缓存"]
DCache["dcache<br/>数据缓存"]
%% 指令通路
Core -->|"icache_pc_w[31:0]<br/>icache_rd_w"| ICache
ICache -->|"icache_inst_w[63:0]<br/>icache_valid_w<br/>icache_accept_w<br/>icache_error_w"| Core
%% 数据通路
Core -->|"dcache_addr_w[31:0]<br/>dcache_data_wr_w[31:0]<br/>dcache_rd_w<br/>dcache_wr_w[3:0]"| DCache
DCache -->|"dcache_data_rd_w[31:0]<br/>dcache_ack_w<br/>dcache_accept_w<br/>dcache_error_w"| Core
%% 控制信号
Core -.->|"icache_flush_w<br/>icache_invalidate_w"| ICache
Core -.->|"dcache_flush_w<br/>dcache_invalidate_w"| DCache
end
%% 外部接口
AXI_I["AXI总线<br/>(指令)"]
AXI_D["AXI总线<br/>(数据)"]
RST["复位/中断<br/>reset_vector_i<br/>intr_i"]
ICache <-->|"axi_i_*"| AXI_I
DCache <-->|"axi_d_*"| AXI_D
RST --> Core
style Core fill:#e1f5ff,stroke:#01579b,stroke-width:3px
style ICache fill:#fff3e0,stroke:#e65100,stroke-width:2px
style DCache fill:#f3e5f5,stroke:#4a148c,stroke-width:2px
style AXI_I fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
style AXI_D fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px
riscv_core 是一个核心,它包含这些部件:
- biriscv_frontend:
- biriscv_mmu:
- biriscv_lsu:
- biriscv_csr:
- biriscv_multiplier:
- biriscv_divider:
- biriscv_issue:
- biriscv_exec:u_exec0,u_exec1
riscv_core 数据通路说明:
- 前端 → 发射:双指令流 (fetch0/fetch1)
- 发射 → 执行:双发射到两个执行单元 + 乘法器 + LSU + CSR
- 执行 → 写回:所有执行单元的结果写回到 Issue 单元
- 分支控制:执行单元计算分支结果反馈到前端
- 访存通路:通过 MMU 连接到外部存储
graph TB
subgraph riscv_core["riscv_core CPU核心"]
direction TB
subgraph Frontend["前端 (Frontend)"]
FE["biriscv_frontend<br/>取指+分支预测+解码"]
end
subgraph Backend["后端 (Backend)"]
MMU["biriscv_mmu<br/>MMU/地址翻译"]
Issue["biriscv_issue<br/>指令发射/寄存器堆"]
subgraph Execution["执行单元"]
Exec0["biriscv_exec<br/>执行单元0<br/>(ALU/Branch)"]
Exec1["biriscv_exec<br/>执行单元1<br/>(ALU/Branch)"]
Mul["biriscv_multiplier<br/>乘法器"]
Div["biriscv_divider<br/>除法器"]
end
LSU["biriscv_lsu<br/>Load/Store单元"]
CSR["biriscv_csr<br/>控制状态寄存器"]
end
%% 取指通路
FE -->|"fetch0/1_valid<br/>fetch0/1_instr[31:0]<br/>fetch0/1_pc[31:0]"|Issue
%% 指令发射
Issue -->|"opcode0_*<br/>(opcode/pc/operands)"| Exec0
Issue -->|"opcode1_*<br/>(opcode/pc/operands)"| Exec1
Issue -->|"mul_opcode_*"| Mul
Issue -->|"lsu_opcode_*"| LSU
Issue -->|"csr_opcode_*"| CSR
Exec0 -.->|"div_opcode_*"| Div
%% 写回通路
Exec0 -->|"writeback_exec0_value_w"| Issue
Exec1 -->|"writeback_exec1_value_w"| Issue
Mul -->|"writeback_mul_value_w"| Issue
Div -->|"writeback_div_value/valid_w"| Issue
LSU -->|"writeback_mem_value/valid_w"| Issue
CSR -->|"csr_result_e1_value_w"| Issue
%% 分支反馈
Exec0 -->|"branch_exec0_*"| Issue
Exec1 -->|"branch_exec1_*"| Issue
Issue -->|"branch_request/pc/priv"| FE
CSR -->|"branch_csr_*"| Issue
%% LSU 访存通路
LSU -->|"mmu_lsu_addr_w<br/>mmu_lsu_data_wr_w<br/>mmu_lsu_rd/wr_w"| MMU
MMU -->|"mmu_lsu_data_rd_w<br/>mmu_lsu_ack_w"| LSU
%% 取指访存通路
FE -->|"mmu_ifetch_pc_w<br/>mmu_ifetch_rd_w"| MMU
MMU -->|"mmu_ifetch_inst_w[63:0]<br/>mmu_ifetch_valid_w"| FE
%% CSR 控制
CSR -.->|"mmu_priv/satp/flush"| MMU
CSR -.->|"take_interrupt<br/>ifence"| Issue
end
%% 外部接口
IMEM["指令存储<br/>(ICache)"]
DMEM["数据存储<br/>(DCache)"]
MMU <-->|"mem_i_pc_o[31:0]<br/>mem_i_inst_i[63:0]"| IMEM
MMU <-->|"mem_d_addr_o[31:0]<br/>mem_d_data_wr/rd_o"| DMEM
style FE fill:#ffe0b2,stroke:#e65100,stroke-width:3px
style Issue fill:#b3e5fc,stroke:#01579b,stroke-width:3px
style Exec0 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style Exec1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style Mul fill:#f0f4c3,stroke:#827717,stroke-width:2px
style Div fill:#f0f4c3,stroke:#827717,stroke-width:2px
style LSU fill:#e1bee7,stroke:#6a1b9a,stroke-width:2px
style CSR fill:#ffccbc,stroke:#bf360c,stroke-width:2px
style MMU fill:#b2dfdb,stroke:#00695c,stroke-width:2px
biriscv_frontend
这里有三个内部模块:biriscv_npc、biriscv_decode、biriscv_fetch。
biriscv_npc
biriscv_npc 的作用是计算 next_pc + 分支预测,将 next_pc_f_o 和 next_taken_f_o 发射到 biriscv_fetch 部件。issue 部件将 npc 反馈信息发到 frontend,最终到达 biriscv_npc。issue 的分支反馈信息来源于 biriscv_exec 执行部件。
biriscv_fetch
fetch 拿到来自 icache 经过 mmu 处理过的地址的指令后,将:
- fetch_valid_w:
- fetch_instr_w[63:0]:
- fetch_pc_w:
等信号发往 biriscv_decode 单元。
biriscv_decode
decode 单元拿到这些信号后,对数据进行解析,然后将数据发送到 issue 模块(双发射)。
graph TB
subgraph biriscv_frontend["biriscv_frontend 前端模块"]
direction TB
NPC["biriscv_npc<br/>下一PC计算<br/>+分支预测<br/>(BTB/BHT/RAS)"]
Fetch["biriscv_fetch<br/>取指单元<br/>PC管理"]
Decode["biriscv_decode<br/>解码单元<br/>(双发射)"]
%% NPC 计算
NPC -->|"next_pc_f_w[31:0]<br/>next_taken_f_w[1:0]<br/>(预测信息)"| Fetch
Fetch -->|"fetch_pc_f_w[31:0]<br/>fetch_pc_accept_w<br/>(当前PC)"| NPC
%% 取指通路
Fetch -->|"fetch_valid_w<br/>fetch_instr_w[63:0]<br/>fetch_pc_w[31:0]<br/
>fetch_pred_branch_w[1:0]"| Decode
%% 解码输出
Decode -->|"fetch0_valid_o<br/>fetch0_instr_o[31:0]<br/>fetch0_pc_o[31:0]
<br/>+控制信号"| Out0["发射单元<br/>Instruction 0"]
Decode -->|"fetch1_valid_o<br/>fetch1_instr_o[31:0]<br/>fetch1_pc_o[31:0]
<br/>+控制信号"| Out1["发射单元<br/>Instruction 1"]
%% 反压信号
Out0 -->|"fetch0_accept_i"| Decode
Out1 -->|"fetch1_accept_i"| Decode
Decode -->|"fetch_accept_w"| Fetch
%% 分支反馈
Branch["分支反馈<br/>(来自执行单元)"]
-.->|"branch_request_i<br/>branch_pc_i[31:0]"| Fetch
Branch -.->|"branch_request_i<br/>branch_pc_i[31:0]"| Decode
BranchInfo["分支结果<br/>(实际执行)"]
-.->|"branch_info_request_i<br/>branch_info_is_taken/not_taken_i<br/>branch_info_
source/pc_i<br/>branch_info_is_call/ret/jmp_i"| NPC
end
%% 外部接口
ICache["ICache/MMU"] <-->|"icache_rd_o<br/>icache_pc_o[31:0]<br/>icache_inst_
i[63:0]<br/>icache_valid_i<br/>icache_accept_i"| Fetch
style NPC fill:#fff9c4,stroke:#f57f17,stroke-width:3px
style Fetch fill:#b2dfdb,stroke:#00695c,stroke-width:3px
style Decode fill:#bbdefb,stroke:#0d47a1,stroke-width:3px
style Out0 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style Out1 fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
style ICache fill:#e1bee7,stroke:#6a1b9a,stroke-width:2px
style Branch fill:#ffccbc,stroke:#bf360c,stroke-width:2px
style BranchInfo fill:#ffccbc,stroke:#bf360c,stroke-width:2px
前端数据通路说明:
- PC 流向:NPC → next_pc_f_w → Fetch → icache_pc_o → ICache
- 指令流向:ICache → icache_inst_i[63:0] → Fetch → fetch_instr_w[63:0] → Decode → fetch0/1_instr_o[31:0]
- 分支预测:NPC 产生预测信息 (next_taken_f_w) → Fetch → Decode
- 分支更新:执行单元分支结果 → branch_info_* → NPC 更新预测器
- 双发射输出:Decode 输出两路指令流 (fetch0/fetch1) 到后端
reset_pc
reset_pc 这个信号很关键,它是 reset 之后 CPU 的初始 pc。查看源码可以看到,这个 PC 可以配置,也可以通过仿真环境往里面传入,testbench.h:
1 | |
这个信号传送到 riscv_core –> biriscv_csr:
1 | |
接着这个信号传递到了 issue:
1 | |
reset 结束后的一个周期,branch_csr_request_i 拉高,pc_x_q 就是 传入的 reset_pc。
接着 issue 会发射这组信号发送到前端 frontend:
1 | |
frontend 拿到这组信号后,发射到 decode 和 biriscv_fetch。
decode 拿到 branch_request_i 之后,传给了 fetch_fifo 的 flush,目的是刷掉 fifo,其它两组信号悬空,没被利用。
biriscv_fetch 拿到信号后,传给关建的三个信号:
1 | |
做一些处理后传给 frontend ,然后传给 MMU,biriscv_fetch:
1 | |
同时,assign pc_f_o = icache_pc_w 将信号发射到 biriscv_npc,下一个周期 npc 也将会给出 next_pc:
1 | |
后端:忽略
新的思路
通过前面的分析,现在基本掌握了 briscv frontend 的基本面貌。再次研究一下关于前端以及和前端有关单元的细节,以便于确定最好的思路。
biriscv_npc
给 fetch 单元发射 next_pc,同时也收到 issue 反馈信息以修正分支预测器。
biriscv_fetch
在 reset 之后,fetch 单元固定向 npc 请求 next_pc。同时,biriscv_fetch 拿到 next_pc 之后,就以这个 pc 访问 icache/mmu,获取指令字。
biriscv_decode
decode 拿到指令字之后,就开始解码,这里是组合电路,因此可以抓取 pc:inst进行检验。在 decode 中实例化了两个 decoder 单元,它们负责解析指令类型,比如:
1 | |
biriscv_issue
拿到 decode 发来的数据后,这里就可以发送到执行单元了。同时,这里会给 biriscv_npc 发送反馈信息以更新 btb 或者强化 bht。
观察波形图
这是双发射架构,因此先研究一个发射单元的数据通路。
exec0
exec0 <—> issue。来自 issue 发射单元的一组输入信号:
1 | |
其中关键的信号是 opcode_pc_i,opcode_opcode_i。前者是当前 exec0 正在处理的指令的 pc,后者是经过 decode 处理后的译码信息。
完成计算的同时(后)它输出了一组关键的信息用来反馈前端:
1 | |
其中以下信息将会延迟一拍返回 issue:
1 | |
而这组信号是组合电路产生的,会在本周期内返回 issue:
1 | |
issue
issue 单元会将以下信息发往到 frontend–>npc:
1 | |
npc 在下一个周期就会完成 BTB、BHT 的更新。
同时 issue 将这个分支信息发送到 decode、fetch 单元。
分支预测器的性能表现
首先,当 npc 中的 branch_request_i 的信号拉高的时候,确实是执行单元拿到了不期待的指令。通过观察波形图,当它拉高的时候,有可能是非分支指令。但是当 branch_request_i 拉高的时候,branch_is_taken_i 或者 branch_is_not_taken_i 拉高,那么一定是分支指令出现了跳转错误。
因此,有一个思路是,统计所有的分支指令和 (branch_request_i && (branch_is_taken_i || branch_is_not_taken_i) 的次数,这就能判断出分支指令预测的准确率。
所有的分支指令如何统计?只需要将 branch_is_taken_i 和 branch_is_not_taken_i 数据加起来就行了,就这么简单。
最终的结果令人满意(方向、目标正确率暂未实现,整体正确率才有意义):
1 | |