数据Cache — Data Cache with Write-Back

写回 + 写分配:DCache的完整状态机

📖 DCache vs ICache

数据Cache比指令Cache复杂得多,因为它需要处理写操作。两个关键策略决定了DCache的行为:

写策略对比: Write-Through (写直达): ┌───────┐ 写 ┌───────┐ 写 ┌───────┐ │ CPU │ ───→ │ Cache │ ───→ │ 主存 │ └───────┘ └───────┘ └───────┘ 每次写同时更新Cache和主存 优点: 一致性简单 缺点: 写延迟高 Write-Back (写回): ← 本课采用 ┌───────┐ 写 ┌───────┐ ┌───────┐ │ CPU │ ───→ │ Cache │ 只在 │ 主存 │ └───────┘ │ Dirty!│ 替换 └───────┘ └───────┘ 时写回 优点: 写带宽低 缺点: 需要脏位管理 写分配策略: Write Miss → 先从主存读入行 → 再修改Cache中的行 No-Write-Allocate → 直接写到主存,不读入Cache 本课: Write-Back + Write-Allocate (最常用组合)
策略组合读命中写命中读未命中写未命中
Write-Through + No-WACacheCache+主存Refill仅主存
Write-Back + WACache仅CacheRefillRefill+写

🔬 DCache状态机

DCache需要3个状态来处理写回和Refill:

DCache状态机: ┌─────────────────────────────────────┐ │ IDLE │ │ 命中: 读/写直接完成 │ │ 未命中: 检查替换行是否脏 │ └──────┬──────────────┬───────────────┘ │ 脏 │ 不脏 ▼ ▼ ┌──────────┐ ┌──────────┐ │ WRITEBACK│ │ REFILL │←──┐ │ 写回脏行 │ │ 从主存读 │ │ └────┬─────┘ └────┬─────┘ │ │完成 │完成 │ └──→ REFILL ──→┘ │ (继续从IDLE) │ IDLE ←───────────────────┘

脏位(Dirty Bit)的作用

🖥️ Verilog实现:写回DCache

// Lesson 37: DCache — write-back, write-allocate, 4-way 16-set
module dcache #(
    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 [31:0]         cpu_wdata_i,
    input  wire                 cpu_we_i,
    input  wire                 cpu_req_i,
    input  wire [1:0]           cpu_size_i,
    output reg                  cpu_hit_o,
    output reg  [31:0]          cpu_rdata_o,
    output reg                  cpu_valid_o,
    // Memory 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  [LINE_W-1:0]   mem_wdata_o,
    output reg                  mem_req_o,
    output reg                  mem_we_o
);
    localparam SETS = 1 << INDEX_W;
    reg valid[0:WAYS-1][0:SETS-1];
    reg dirty[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];

    // Tag比较 + 命中检测 (同ICache)
    // ... 省略,同ICache结构

    // 状态机: IDLE → WRITEBACK → REFILL → IDLE
    localparam S_IDLE=0, S_WB=1, S_REFILL=2;
    reg [1:0] state;
    reg [$clog2(WAYS)-1:0] evict_way;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= S_IDLE; /* ... reset all ... */
        end else case (state)
        S_IDLE: begin
            if (cpu_req_i && is_hit) begin
                if (cpu_we_i) begin
                    dirty[hit_way][idx] <= 1;  // 写命中→脏
                    // 更新行内对应word
                    case (off[3:2])
                        0: data[hit_way][idx][31:0]  <= cpu_wdata_i;
                        1: data[hit_way][idx][63:32] <= cpu_wdata_i;
                        2: data[hit_way][idx][95:64] <= cpu_wdata_i;
                        3: data[hit_way][idx][127:96]<= cpu_wdata_i;
                    endcase
                end
                lru[idx] <= hit_way + 1;
            end else if (cpu_req_i && !is_hit) begin
                evict_way <= lru[idx];
                if (dirty[lru[idx]][idx]) begin
                    state <= S_WB;  // 脏→先写回
                    mem_we_o <= 1; mem_req_o <= 1;
                    mem_addr_o <= {tag[lru[idx]][idx],idx,{OFFSET_W{1'b0}}};
                    mem_wdata_o <= data[lru[idx]][idx];
                end else begin
                    state <= S_REFILL;  // 不脏→直接Refill
                    mem_req_o <= 1; mem_we_o <= 0;
                    mem_addr_o <= {rtag,idx,{OFFSET_W{1'b0}}};
                end
            end
        end
        S_WB: if (mem_valid_i) begin
            state <= S_REFILL;
            mem_req_o <= 1; mem_we_o <= 0;
            mem_addr_o <= {rtag,idx,{OFFSET_W{1'b0}}};
        end
        S_REFILL: if (mem_valid_i) begin
            valid[evict_way][idx] <= 1;
            dirty[evict_way][idx] <= 0;
            tag[evict_way][idx]   <= rtag;
            data[evict_way][idx]  <= mem_rdata_i;
            lru[idx] <= evict_way + 1;
            state <= S_IDLE; mem_req_o <= 0;
        end
        endcase
    end
endmodule
Verilator仿真验证通过 — 写回+写分配正确,脏行替换时先写回主存

📊 DCache写策略性能对比

指标Write-ThroughWrite-Back
写延迟高(每次写等主存)低(只写Cache)
写带宽高(每写必传主存)低(只在替换时写回)
一致性简单(Cache=主存)复杂(需要脏位+写回)
功耗
适用场景I/O设备、调试通用处理器
几乎所有现代通用处理器的L1 DCache都使用Write-Back + Write-Allocate组合。Write-Through主要用于I/O区域(Uncached空间)或简单的嵌入式处理器。

🧪 实验练习

  1. 实现字节/半字写:支持SB/SH指令(Read-Modify-Write)
  2. 添加Store Buffer:写命中时不等写完成,缓冲到Store Buffer
  3. 实现临界字优先:Refill时先返回缺失的word,不等整行
  4. 测量写回频率:统计脏行替换占比
写回+写分配正确
思考题:为什么Write-Back通常和Write-Allocate搭配?如果Write-Back + No-Write-Allocate会有什么问题?
参考资料:Hennessy & Patterson §B.2 | ARM A53 DCache | RISC-V CMO Spec