跳轉到

05.3 期末專題:微架構效能量測與改進#

學習目標#

讀完本節後,你應該能夠:

  1. 說明基準測試(benchmark)的意義,並將 Dhrystone 與 CoreMark 移植到自製 RISC-V SoC 上跑分。
  2. 設計自訂指令/協同處理器(co-processor),以 CRC 與 FFT 為例完成 Verilog 實作與軟體整合。
  3. 量化「改進前 vs 改進後」的效能差異,並用 Amdahl 定律分析加速的理論上限。
  4. 規劃期末專題方向,包含流水線加寬、快取相聯度、SIMD 自訂指令等加分主題。

5.3.1 效能量測的方法論#

為什麼要量測?#

前面章節我們能「跑起來」了,但計算機結構的最終問題是:這樣的微架構,效能好不好?改哪裡最划算? 直覺猜測經常是錯的——例如你以為加個硬體乘法器會大幅加速,實測發現程式 90% 時間在等 UART,加速器根本沒幫助。因此必須:

  1. 建立基準(baseline):用相同的測試程式量測改進前的效能。
  2. 找出瓶頸:用硬體計數器或軟體計時,找出時間花在哪。
  3. 單一變數改進:一次只改一個地方,重新量測。
  4. 對比與分析:用表格量化差異,並解釋為什麼。

Dhrystone 與 CoreMark#

兩者是嵌入式處理器最常用的整數基準測試:

項目 Dhrystone CoreMark
年代 1988 2009(EEMBC)
內容 字串處理、指標運算 狀態機、鏈結串列、矩陣運算、CRC
輸出單位 DMIPS/MHz CoreMark/MHz
常見問題 易被編譯器優化「作弊」 較不易作弊,更現代
移植難度 極低 低(需提供计时函数)

移植 CoreMark 到自製 SoC#

CoreMark 原始碼極小(約 12KB),只需提供三個掛勾函式。核心修改在 core_portme.c:

/* core_portme.c —— 移植到自製 RISC-V SoC */
#include "coremark.h"
#include "uart.h"

#define NSECS_PER_SEC 100000000   /* 100MHz SoC */

/* 時間來源:CSR cycle 計數器(見下方) */
static unsigned long long begin_cycles, stop_cycles;

__attribute__((noinline))
void start_time(void) {
    unsigned hi, lo;
    asm volatile ("rdcycleh %0" : "=r"(hi));
    asm volatile ("rdcycle  %0" : "=r"(lo));
    begin_cycles = ((unsigned long long)hi << 32) | lo;
}

__attribute__((noinline))
void stop_time(void) {
    unsigned hi, lo;
    asm volatile ("rdcycleh %0" : "=r"(hi));
    asm volatile ("rdcycle  %0" : "=r"(lo));
    stop_cycles = ((unsigned long long)hi << 32) | lo;
}

CORETIMETYPE get_time(void) {
    return (CORETIMETYPE)(stop_cycles - begin_cycles);
}

void portable_fini(core_results *res) {
    double secs  = (double)res->time / NSECS_PER_SEC;
    double score = (double)res->iterations / secs;
    uart_puts("CoreMark 1.0 : ");
    print_u32((uint32_t)score);          /* 自行實作整數轉字串 */
    uart_puts(" / 100MHz = ");
    print_u32((uint32_t)(score / 100));
    uart_puts(" CoreMark/MHz\r\n");
}

前提:CPU 必須實作 cycle CSR(位址 0xC00 與 0xC80)。這是 RISC-V 標準規定的效能計數器,在 05.1 的 SoC 中加入:

// 在 CPU 內部(或 CSR 單元)加上:
reg [63:0] cycle_count;
always @(posedge clk) cycle_count <= cycle_count + 64'd1;
// Zicsr 讀取:0xC00 -> cycle, 0xC80 -> cycleh

教學註記:若你的 CPU 尚未實作 Zicsr,可用 memory-mapped 計數器(如 0x1000_0020)替代,軟體用 lw 讀取即可,概念相同。

編譯旗標(與 05.2 相同基礎):

riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 -O2 \
    -ffunction-sections -fdata-sections -frename-registers \
    -DFLAGS_STR='"-O2 rv32im"' \
    core_main.c core_list_join.c core_matrix.c core_state.c \
    core_util.c core_portme.c uart_min.c crt0.S \
    -T link.ld -nostdlib -ffreestanding -o coremark.elf

執行後得到第一組基準數據(範例,單週期 CPU、無快取):

配置 CoreMark/MHz 說明
單週期 CPU @ 25MHz(BRAM 限制) 0.35 每指令 1 週期但時脈低
5 級流水線 @ 80MHz 1.20 時脈提升,但分支冒險損失
流水線 + 分支預測 1.45 減少分支代價
流水線 + 預測 + 自訂 CRC 指令 1.58 加速器只幫助 CRC 部分

5.3.2 自訂指令與協同處理器#

概念:RoCC 式協同處理器#

RISC-V 預留了自訂指令編碼空間(custom-0=opcode 0x0B、custom-1=0x2B),允許設計者加入硬體加速指令。常見形式:

  1. Co-processor 介面(如 Rocket 的 RoCC):CPU 解碼自訂指令,將 rs1/rs2 值交給協同器,協同器回傳結果。
  2. 緊耦合功能單元:直接在 ALU 旁加一個單元,最省延遲。

協定:CPU 發出 cop_valid, cop_func[2:0], cop_rs1, cop_rs2;協同器算完後以 cop_ready, cop_result 回應;期間 CPU 停等(stall)。

範例一:CRC32 指令#

CRC(循環冗餘校驗)是通訊與儲存必備。軟體逐位元計算需 32 次迴圈;硬體可一拍算 8 位元。定義指令:

  • 語意:rd = crc32_update(rd, rs1) —— 以 rs1 為輸入 byte,更新 rd 中的 CRC 狀態(多項式 0x04C11DB7)。
// crc_cop.v —— CRC32 協同處理器(每拍處理 8 位元)
module crc_cop (
    input  wire        clk,
    input  wire        valid,     // CPU 發出自訂指令
    input  wire [2:0]  func,
    input  wire [31:0] rs1_data,  // 新資料 byte(低 8 位元)
    input  wire [31:0] rs2_data,  // 目前的 CRC 狀態
    output reg         ready,
    output reg  [31:0] result
);
    function [31:0] crc32_byte;
        input [31:0] crc;
        input [7:0]  data;
        reg [31:0] x;
        integer i;
        begin
            x = crc ^ {data, 24'b0};
            for (i = 0; i < 8; i = i + 1)
                x = x[31] ? (x << 1) ^ 32'h04C11DB7 : (x << 1);
            crc32_byte = x;
        end
    endfunction

    always @(posedge clk) begin
        ready  <= 1'b0;
        if (valid && func == 3'b000) begin
            ready  <= 1'b1;
            result <= crc32_byte(rs2_data, rs1_data[7:0]);
        end
    end
endmodule

CPU 中的解碼(在 ID 級):

wire is_custom = (instr[6:0] == 7'b0001011);   // custom-0
wire [2:0] cust_func = instr[14:12];
wire crc_sel = is_custom && (cust_func == 3'b000);
// 使流水線停等 1 拍,然後將 crc_result 寫回 rd

軟體端(內嵌組語包裝):

static inline uint32_t crc32_acc(uint32_t crc, uint8_t byte) {
    uint32_t out;
    asm volatile (".insn r 0x0B, 0, 0, %0, %1, %2"
                  : "=r"(out) : "r"(byte), "r"(crc));
    return out;
}

uint32_t crc32_buf(const uint8_t *p, int n) {
    uint32_t crc = 0xFFFFFFFF;
    for (int i = 0; i < n; i++)
        crc = crc32_acc(crc, p[i]);
    return ~crc;
}

.insn r opcode, func3, func7, rd, rs1, rs2 是 GCC 的自訂指令語法,讓你在 C 中直接使用硬體指令,無需修改編譯器。

範例二:FFT 蝶形運算指令#

FFT 的核心是蝶形(butterfly):

Xr' = Xr + (Wr*Xi_r - Wi*Xi_i)
Xi' = Xi + (Wr*Xi_i + Wi*Xi_r)

可設計一條指令完成「複數乘法的一部分」,或更簡單地提供 cmul(16 位元定點複數乘法):

// cmul_cop.v —— 16-bit 定點複數乘法:(ar+ai j)*(br+bi j)
module cmul_cop (
    input  wire        clk,
    input  wire        valid,
    input  wire [31:0] a,        // {ai[15:0], ar[15:0]}
    input  wire [31:0] b,        // {bi[15:0], br[15:0]}
    output reg         ready,
    output reg  [31:0] real_part // 僅回傳實部;虛部由第二條指令算
);
    wire signed [15:0] ar = a[15:0],  ai = a[31:16];
    wire signed [15:0] br = b[15:0],  bi = b[31:16];
    wire signed [31:0] pr = ar*br - ai*bi;
    always @(posedge clk) begin
        ready <= 1'b0;
        if (valid && func == 3'b001) begin
            ready <= 1'b1;
            real_part <= pr[29:14];   // Q15 定點,右移 14 回到 Q15
        end
    end
endmodule

(完整 FFT 引擎也可以做成 DMA 式加速器:CPU 丟進資料指標、啟動後輪詢完成旗標。兩種架構各有取捨:自訂指令延遲低但佔用 CPU;DMA 加速器可並行但需匯流排仲裁。)

為什麼加速「有效」?——Amdahl 定律#

若程式在 CRC 部分花費比例為 \(f\) ,該部分加速 \(s\) 倍,則整體加速:

\[ S = \frac{1}{(1-f) + \frac{f}{s}} \]

實測中,CoreMark 的 CRC 單元約佔總時間 20%,若 CRC 硬體使其加速 10 倍,理論整體加速為 \(1/(0.8+0.02)\approx 1.22\) 倍。這提醒我們:加速冷門程式碼效果有限,選對目標(f 大的部分)才有意義。


5.3.3 效能前後對比(範例數據)#

以下為 5 級流水線 RV32IM 在 Arty A7-35T 上的量測範例(100MHz 目標):

改進項目 CoreMark/MHz Dhrystone DMIPS/MHz LUT 數 最高時脈
基準(5 級流水,無預測) 1.20 0.90 2400 80 MHz
+ 分支預測(BTB 16 項 + BHT) 1.45 1.10 2750 78 MHz
+ CRC 自訂指令 1.58 1.12 2950 78 MHz
+ cmul 自訂指令 1.61 1.13 3150 76 MHz
+ 2 路組相聯 I-Cache(4KB) 1.78 1.40 4100 76 MHz

觀察:

  • 分支預測 CP 值最高——分支在 CoreMark 中出現頻率高(約 15–20% 指令)。
  • 自訂指令提升有限但穩定,符合 Amdahl 分析。
  • 快取在純 BRAM 系統提升不若 DRAM 系統明顯,但為將來外接 SDRAM 打基礎。

5.3.4 期末專題建議清單#

基本要求(擇一)#

  1. 微架構改進:在既有流水線 CPU 上實作一項改進(分支預測/轉發優化/load-use 氣泡消除),量測前後 CoreMark。
  2. 自訂指令加速器:選一個演算法(CRC、AES S-Box、FFT 蝶形、popcount、位元反轉),設計自訂指令並完成軟硬整合與對比量測。
  3. 周邊整合:為 SoC 加上新周邊(SPI、PS/2 鍵盤、VGA 文字模式、Timer+中斷),並寫出對應驅動程式。

加分項目#

  1. 流水線加寬:從 5 級擴充為 6–7 級(分離 MEM 級、加入額外轉發路徑),或實作雙發射(dual-issue):同一拍發射兩條無相依指令。量測 IPC(每週期指令數)變化。
  2. 快取相聯度實驗:實作直接映射 vs 2 路/4 路組相聯 I-Cache/D-Cache,用 CoreMark 與矩陣乘法(不同資料走勢)比較 miss rate 與 CPI,討論相聯度與面積/時序的取捨。
  3. SIMD 自訂指令:定義一組 32-bit 內打包 4×8-bit 或 2×16-bit 的 SIMD 指令(如飽和加法、乘加),實作硬體並用它加速影像灰階轉換或 FIR 濾波,對比純標量版本的速度。
  4. 中斷與例外:實作完整的 CSR、mtvec、中斷嵌套,寫一個 timer 中斷驅動的多工迷你排程器。
  5. 軟體工具鏈:寫一個小型 bootloader,支援從 UART 載入程式(見 05.2 習題 5)並結合 QSPI flash 存放多個應用。

評分建議#

項目 比重
功能正確性(上板 demo) 40%
量測嚴謹度(基準/對比/分析) 25%
報告品質(理論分析、取捨討論) 25%
加分創意 10%+

本章小結#

  • 效能改進必須「先量測、再改進、再驗證」;CoreMark/MHz 是可比較的標準指標,移植只需提供 start_time/stop_time 等掛勾與 cycle CSR。
  • RISC-V 的 custom opcode 加上 .insn 內嵌組語,是最低成本的自訂指令整合路徑;CRC 一拍一字元是典型範例。
  • Amdahl 定律提醒:加速必須針對時間占比大的部分,否則事倍功半。
  • 好的期末專題 = 明確的改進假設 + 可重現的量測 + 深入的取捨分析,而非功能堆疊。

實作題#

  1. cycle CSR:為你的 CPU 加入 cycle/cycleh CSR,並寫一段程式量測 100 萬次迴圈所耗時脈數,計算出 CPI,與理論值比較。
  2. CRC 加速:完整實作 5.3.2 的 CRC32 自訂指令,用 1KB 資料量測「軟體版 vs 硬體版」的執行週期數,並代入 Amdahl 定律驗證整體加速是否符合預測。
  3. FFT 加速:實作 cmul 指令(含虛部指令),完成一個 64 點定點 FFT,比較使用前後的週期數;若時間充裕,將 FFT 做成 DMA 式加速器,比較兩種架構的總時間與 CPU 佔用率。
  4. 快取相聯度:實作 1KB 直接映射與 1KB 2 路組相聯 I-Cache,用 CoreMark 與轉置矩陣乘法兩種 workload 量測 miss rate,解釋為何不同 workload 下結果不同。
  5. SIMD 指令:定義並實作 simd_add16(32-bit 內兩個 16-bit 飽和加法)與 simd_mul16,用它加速 64-tap FIR 濾波,繪製「標量 vs SIMD」的週期數對比長條圖。