冒險與繞送#
五階段管線的漂亮圖景,遇到指令之間的依賴就出現裂縫。這些裂縫叫 hazard(冒險),處理它們的電路叫 hazard unit。這一節講三類冒險與對策,並給出 hazard unit 的 Veryl 骨架。
三類冒險#
- 結構冒險(structural hazard):兩條指令搶同一個硬體資源。例如暫存器堆同時被 ID 讀、WB 寫——正統解法是把暫存器堆做成「前半拍寫、後半拍讀」,或加寫優先邏輯。
- 資料冒險(data hazard):後面的指令需要前面指令還沒寫回的結果。
add x1, x2, x3緊跟sub x4, x1, x5:sub 在 EX 需要 x1,而 add 的結果還在 EX/MEM 暫存器裡,要到 WB 結束才進暫存器堆。 - 控制冒險(control hazard):分支指令要到 EX 才知道要不要跳,但 IF 已經預取了錯的指令。
Forwarding(繞送/旁路)#
資料冒險最優雅的解法是 forwarding:結果既然已經算出來、就躺在 EX/MEM 或 MEM/WB 暫存器裡,直接從那裡繞一條線到 EX 的 ALU 輸入,不必等它慢悠悠走完 WB。兩條經典旁路:
- EX/MEM → EX:前一條指令的 ALU 結果,餵給當前指令的 ALU 輸入。
- MEM/WB → EX:前兩條指令的結果,同樣可以旁路。
forwarding 解決大多數情況,唯獨 load-use 冒險無解:load 的資料要到 MEM 級結束才存在,而緊跟其後的指令在下一拍就要進 EX。唯一的辦法是 stall(氣泡)一拍。硬體偵測「EX 級的來源暫存器 == MEM 級 load 的目的暫存器」,就凍結 PC 與 IF/ID,並給 ID/EX 塞一個 bubble。
Hazard unit 骨架#
import cpu_pkg::*;
module HazardUnit (
// 來自各級的控制訊號
ex_rs1: input logic<5>,
ex_rs2: input logic<5>,
mem_rd: input logic<5>,
mem_mem_read: input logic, // MEM 級是 load?
mem_reg_write: input logic,
wb_rd: input logic<5>,
wb_reg_write: input logic,
id_branch: input logic, // 解碼出 branch/jump?
// 對外的控制
forward_a: output logic<2>, // 00:regfile 01:mem 10:wb
forward_b: output logic<2>,
stall: output logic,
flush: output logic,
) {
const FWD_REG: logic<2> = 2'b00;
const FWD_MEM: logic<2> = 2'b01;
const FWD_WB: logic<2> = 2'b10;
always_comb {
// 預設:都從暫存器堆拿
forward_a = FWD_REG;
forward_b = FWD_REG;
stall = 0;
flush = 0;
// EX 冒險:前一條(MEM 級)可繞送
if mem_reg_write && mem_rd != 0 && mem_rd == ex_rs1 {
forward_a = FWD_MEM;
}
if mem_reg_write && mem_rd != 0 && mem_rd == ex_rs2 {
forward_b = FWD_MEM;
}
// WB 級旁路(優先級較低)
if wb_reg_write && wb_rd != 0 && wb_rd == ex_rs1 && !(mem_reg_write && mem_rd == ex_rs1) {
forward_a = FWD_WB;
}
if wb_reg_write && wb_rd != 0 && wb_rd == ex_rs2 && !(mem_reg_write && mem_rd == ex_rs2) {
forward_b = FWD_WB;
}
// load-use:必須 stall 一拍
if mem_mem_read && mem_rd != 0 && (mem_rd == ex_rs1 || mem_rd == ex_rs2) {
stall = 1;
forward_a = FWD_REG;
forward_b = FWD_REG;
}
// 控制冒險:分支就把 IF/ID 洗掉
if id_branch {
flush = 1;
}
}
}
注意兩個細節。第一,wb_rd != 0:x0 恆為零,任何對 x0 的寫都不該觸發旁路——這是新手最常漏的條件。第二,MEM 級旁路優先於 WB 級:較新的資料優先。這種優先編碼邏輯非常適合在驗證裡以隨機指令序列狂打。
六十年的老問題#
資料冒險不是 RISC 時代的新發明。1964 年問世的 CDC 6600(Seymour Cray 設計)就配備了 scoreboard——一個集中追蹤各運算元就緒狀態的機制,讓不依賴的指令可以亂序推進。1967 年 IBM System/360 Model 91 的 Tomasulo 演算法更進一步:把 scoreboard 的中央控制分散到每個保留站(reservation station),用暫存器改名(register renaming)消除假相依。這兩個系統是六十年前用離散電晶體與 ECL 電路打造的,其思想直接延續到今天的亂序核心——Intel/AMD/Apple 的高性能 CPU 內部,跑的仍是 scoreboard 與 Tomasulo 的直系子孫。
順序五階段管線只需 forwarding 與 stall,是這座思想金字塔的第一層台階。把這層蓋穩,往上的亂序、超純量、多核,才站得住。下一節,我們把核心送上 FPGA。