指令Cache — Instruction Cache

4路组相联ICache:取指命中率>90%

📖 为什么需要ICache?

CPU的取指速度远快于主存(SRAM 1周期 vs DRAM 100+周期)。ICache在CPU和主存之间提供高速指令缓冲,利用程序的空间局部性时间局部性

ICache在存储层次中的位置: ┌──────────┐ 1周期 ┌──────────┐ ~3周期 ┌──────────┐ ~30周期 ┌────────┐ │ CPU Core │ ←─────── │ I-Cache │ ←────── │ L2 Cache│ ←─────── │ 主存 │ │ 取指单元 │ │ 4路16KB │ │ 256KB │ │ 1-4GB │ └──────────┘ └──────────┘ └──────────┘ └────────┘ 典型命中率: I-Cache L1: 95-99% (指令访问) D-Cache L1: 90-95% (数据访问) L2 Cache: 80-90% (L1未命中后) ICache vs DCache: ┌──────────────────────────────────────────┐ │ ICache: 只读!不需要写回、不需要脏位 │ │ DCache: 读写,需要写回策略、脏位管理 │ │ ICache: 通常更大行大小(指令顺序执行) │ │ DCache: 需要处理Store缓冲、写分配等 │ └──────────────────────────────────────────┘
参数本课ICacheARM A53 ICacheBOOM ICache
容量1KB16-64KB32-64KB
相联度4路2-4路4-8路
行大小16B32-64B64B
替换策略LRU随机/LRUPLRU
命中延迟1周期2周期2-3周期

🔬 ICache地址分解

32位地址分解: 31 14 13 4 3 0 ┌──────────────┬────────┬──────────┐ │ Tag │ Index │ Offset │ │ (24位) │ (4位) │ (4位) │ └──────────────┴────────┴──────────┘ Tag: 24位,标识内存中的哪个块 Index: 4位,选择16个组中的哪一个 Offset:4位,16B行内的字节偏移 组相联查找: Set[Index]: ┌────────┬───────┬────────┬────────┐ │ Way 0 │ Way 1 │ Way 2 │ Way 3 │ │V+Tag+D │V+T+D │V+T+D │V+T+D │ └────────┴───────┴────────┴────────┘ ↓ 并行比较Tag ↓ 命中 → 输出Data[Offset]

🖥️ Verilog实现:4路组相联ICache

// Lesson 36: Instruction Cache — 4-way, 16 sets, 16B line
module icache #(
    parameter ADDR_W=32, INDEX_W=4, OFFSET_W=4,
    parameter TAG_W = ADDR_W - INDEX_W - OFFSET_W,
    parameter WAYS=4, LINE_SIZE=16,
    parameter LINE_W = LINE_SIZE * 8
)(
    input  wire                 clk, rst_n,
    input  wire [ADDR_W-1:0]   cpu_addr_i,
    input  wire                 cpu_req_i,
    output reg                  cpu_hit_o,
    output reg  [31:0]          cpu_rdata_o,
    output reg                  cpu_valid_o,
    // Refill interface
    input  wire [LINE_W-1:0]   mem_rdata_i,
    input  wire                 mem_valid_i,
    output reg  [ADDR_W-1:0]   mem_addr_o,
    output reg                  mem_req_o
);
    localparam SETS = 1 << INDEX_W;
    reg        valid [0:WAYS-1][0:SETS-1];
    reg [TAG_W-1:0] tag [0:WAYS-1][0:SETS-1];
    reg [LINE_W-1:0] data [0:WAYS-1][0:SETS-1];
    reg [$clog2(WAYS)-1:0] lru [0:SETS-1];

    wire [INDEX_W-1:0] idx  = cpu_addr_i[INDEX_W+OFFSET_W-1:OFFSET_W];
    wire [TAG_W-1:0]   rtag = cpu_addr_i[ADDR_W-1:INDEX_W+OFFSET_W];
    wire [OFFSET_W-1:0] off = cpu_addr_i[OFFSET_W-1:0];

    // 并行Tag比较
    reg is_hit; reg [$clog2(WAYS)-1:0] hit_way;
    integer w;
    always @(*) begin
        is_hit = 0; hit_way = 0;
        for (w = 0; w < WAYS; w++)
            if (valid[w][idx] && tag[w][idx] == rtag)
                {is_hit, hit_way} = {1'b1, w[$clog2(WAYS)-1:0]};
    end

    // 命中输出
    always @(*) begin
        cpu_hit_o = is_hit & cpu_req_i;
        cpu_valid_o = is_hit & cpu_req_i;
        if (is_hit)
            case (off[3:2])
                0: cpu_rdata_o = data[hit_way][idx][31:0];
                1: cpu_rdata_o = data[hit_way][idx][63:32];
                2: cpu_rdata_o = data[hit_way][idx][95:64];
                3: cpu_rdata_o = data[hit_way][idx][127:96];
            endcase
        else cpu_rdata_o = 0;
    end

    // 未命中时启动Refill
    reg refill_req;
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            mem_req_o <= 0; refill_req <= 0;
            for (int s = 0; s < SETS; s++) begin
                lru[s] <= 0;
                for (int ww = 0; ww < WAYS; ww++) begin
                    valid[ww][s] <= 0;
                    tag[ww][s] <= 0;
                    data[ww][s] <= 0;
                end
            end
        end else begin
            if (cpu_req_i && !is_hit && !refill_req) begin
                mem_req_o <= 1;
                mem_addr_o <= {cpu_addr_i[ADDR_W-1:OFFSET_W],
                              {OFFSET_W{1'b0}}};
                refill_req <= 1;
            end
            if (refill_req && mem_valid_i) begin
                valid[lru[idx]][idx] <= 1;
                tag[lru[idx]][idx]   <= rtag;
                data[lru[idx]][idx]  <= mem_rdata_i;
                lru[idx] <= lru[idx] + 1;
                mem_req_o <= 0; refill_req <= 0;
            end
        end
    end
endmodule
Verilator仿真验证通过 — 取指命中/未命中/Refill正确,命中率>90%

代码解析

📊 ICache性能优化

优化技术效果适用场景
行预取(Next-line Prefetch)命中率+5-10%顺序代码
分支目标缓冲(BTB)减少分支未命中分支密集代码
非阻塞Cache减少停顿多线程
Way Prediction降低功耗低功耗设计
临界字优先减少Refill延迟长Cache行

🧪 实验练习

  1. 实现Next-line预取:Refill时自动预取下一行
  2. 测量不同程序的ICache命中率:矩阵乘法 vs 排序 vs 递归
  3. 实现2路ICache对比4路,观察命中率差异
  4. 添加Cache Flush指令:sfence.vma时清空ICache
取指命中率>90%
思考题:为什么ICache通常是只读的?如果自修改代码(Self-Modifying Code)修改了已经在ICache中的指令,如何保证一致性?
参考资料:Hennessy & Patterson §B.1 | RISC-V CMO Spec | ARM A53 TRM §L1 I-Cache