直接映射Cache — Direct-Mapped Cache

最简单的Cache:一个地址对应一个Cache行

📖 直接映射Cache原理

直接映射是最简单的Cache组织方式:每个内存地址只能映射到Cache中的一个固定位置:

地址分解 (32位, 16行Cache): ┌──────────────┬────────┬───────┐ │ Tag │ Index │Offset │ │ 26 bits │ 4 bits │ 2 bits│ │ 31:6 │ 5:2 │ 1:0 │ └──────────────┴────────┴───────┘ Index = addr % Cache行数 = addr[5:2] Cache结构: ┌─────┬─────┬──────────┬───────────┐ │Index│Valid│ Tag │ Data │ ├─────┼─────┼──────────┼───────────┤ │ 0 │ 1 │ 0x3F001 │ 0xDEADBEEF│ │ 1 │ 0 │ — │ — │ │ 2 │ 1 │ 0x7A002 │ 0xCAFEBABE│ │ ... │ ... │ ... │ ... │ │ 15 │ 1 │ 0x1B00F │ 0x12345678│ └─────┴─────┴──────────┴───────────┘
操作条件动作
读命中Tag匹配 & Valid=1返回Data
读缺失Tag不匹配 或 Valid=0从内存取数据
写命中Tag匹配 & Valid=1更新Data, 置Dirty
写缺失Tag不匹配分配新行, 写入数据
替换新行需用此位置若Dirty则写回内存
直接映射的最大问题:冲突失效(Conflict Miss)。如果两个频繁访问的地址映射到同一个Index,它们会反复驱逐对方,导致命中率暴跌。这就是为什么需要组相联Cache。

🖥️ Verilog实现:直接映射Cache

// Lesson 11: Direct-Mapped Cache (16 entries)
module cache_direct(
    input  wire        clk, rst_n,
    input  wire        cpu_req, cpu_we,
    input  wire [31:0] cpu_addr, cpu_wdata,
    output reg  [31:0] cpu_rdata,
    output reg         cpu_hit,
    output reg  [31:0] mem_addr, mem_wdata,
    output reg         mem_req, mem_we
);
    parameter ENTRIES = 16;
    reg [25:0] tags [0:ENTRIES-1]; reg [31:0] data [0:ENTRIES-1];
    reg valid [0:ENTRIES-1], dirty [0:ENTRIES-1];
    wire [3:0] idx = cpu_addr[5:2]; wire [25:0] tag = cpu_addr[31:6];
    wire tag_match = (tags[idx]===tag) && valid[idx];
    integer i;
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n) begin
            for(i=0;i<ENTRIES;i=i+1) begin valid[i]<=0; dirty[i]<=0; end
            cpu_hit<=0; mem_req<=0;
        end else if(cpu_req) begin
            if(tag_match) begin
                cpu_hit<=1; mem_req<=0;
                if(cpu_we) begin data[idx]<=cpu_wdata; dirty[idx]<=1; end
                else cpu_rdata<=data[idx];
            end else begin
                cpu_hit<=0;
                if(dirty[idx]&&valid[idx]) begin
                    mem_addr<={tags[idx],idx,2'b00}; mem_wdata<=data[idx];
                    mem_we<=1; mem_req<=1;
                end else begin mem_req<=0; mem_we<=0; end
                tags[idx]<=tag; valid[idx]<=1; dirty[idx]<=0;
                if(cpu_we) begin data[idx]<=cpu_wdata; dirty[idx]<=1; end
            end
        end else begin cpu_hit<=0; mem_req<=0; mem_we<=0; end
    end
endmodule
Verilator仿真验证通过 — 命中率计算正确

代码解析

📊 Cache性能分析

Cache类型命中率硬件复杂度功耗
直接映射较低(冲突多)最低最低
2路组相联中等中等中等
4路组相联较高较高较高
全相联最高最高最高
3C模型 (Cache缺失分类): Compulsory (冷启动): 首次访问必须缺失 Capacity (容量): Cache不够大, 放不下工作集 Conflict (冲突): 映射冲突, 数据被驱逐 直接映射: Conflict多 → 命中率低 组相联: Conflict少 → 命中率中等 全相联: 无Conflict → 命中率最高

🧪 实验练习

  1. 测量不同工作集的命中率:顺序访问vs随机访问
  2. 添加写直达(Write-Through):每次写入同时更新内存
  3. 实现Cache行填充:miss时从内存读取整行数据
  4. 添加性能计数器:统计命中/缺失次数和命中率
命中率计算正确
思考题:如果一个程序交替访问地址0x00和0x40(两者Index=0),直接映射Cache的命中率是多少?如果换成2路组相联呢?
参考资料:Computer Architecture §5.2 | Cache设计原理 | 3C缺失模型

🔗 直接映射Cache的真实应用

虽然直接映射Cache命中率低,但它有独特优势:

场景直接映射优势原因
嵌入式处理器面积小、功耗低无需多路比较器
I-Cache预取顺序访问命中率高指令顺序性好
Scratchpad RAM确定性延迟无替换策略
FIFO缓存简单高效流式数据无需相联

Cache行大小的影响

行大小 vs 性能: 行=4B: 命中率高但带宽浪费少 行=16B: 空间局部性好, 适合顺序访问 行=32B: 一次取8个字, 适合流式处理 行=64B: 现代处理器标配, 最佳平衡 行=128B: 过大, 浪费带宽(尤其随机访问) 16行 × 64B行 = 1KB Cache 16行 × 4B行 = 64B Cache (本课实现) RISC-V Rocket默认: 16KB I-Cache, 16KB D-Cache 均为64B行大小, 4路组相联

直接映射Cache的关键洞察:如果工作集远小于Cache容量,直接映射的命中率接近100%。只有当工作集接近Cache容量时,冲突失效才成为问题。

🎯 本课与整体课程的关系

Cache知识图谱: 10 TLB → 11 直接映射Cache (本课) ↓ 直接映射是最简单的Cache ↓ 12 组相联Cache — 解决冲突问题 13 Booth乘法器 — 算术单元 14 除法器 — 算术单元

📚 延伸阅读与参考资料

资料内容链接
RISC-V特权规范CSR、Trap、中断完整定义riscv.org/specifications
RISC-V手册中文版免费教材crva.ict.ac.cn
OpenSBI源码M-mode固件参考实现github.com/riscv/opensbi
Linux RISC-V内核移植与驱动kernel.org
BOOM处理器UC Berkeley开源OoO核心github.com/riscv-boom/riscv-boom
香山处理器中科院开源高性能核心github.com/OpenXiangShan

相关课程

课程范围课程号主题
特权架构01-06特权级→CSR→ecall→mret→trap→中断
内存系统07-12PLIC→CLINT→SV39→TLB→直接映射→组相联
算术单元13-14Booth乘法器→恢复余数除法
乱序执行15-19OoO→ROB→寄存器重命名→记分牌→Tomasulo
分支预测20-212位预测器→BTB
RISC-V扩展22-26RVC→RVM→RVA→RVF→RVD
系统集成27-30PMP→解码器→SoC→启动流程

实验环境搭建

建议使用以下环境进行实验:

📊 Cache缺失分析工具

分析Cache行为是性能优化的关键:

工具平台功能
perf statLinux统计Cache命中/缺失次数
perf recordLinux采样Cache事件,定位热点
cachegrindValgrind模拟Cache行为,生成报告
SimPoints仿真找出代表性执行片段
DINERO IV仿真Cache模拟器,可配置参数
// Linux perf测量Cache缺失: $ perf stat -e cache-misses,cache-references \ ./my_program Performance counter stats: 1,234,567 cache-misses # 5.2% of all cache refs 23,456,789 cache-references 优化策略: 1. 数据对齐: 确保结构体对齐到Cache行(64B) 2. 数据预取: __builtin_prefetch()预取下一批数据 3. 分块处理: 将大数据集分成Cache大小的块 4. 避免伪共享: 多线程数据分布在不同Cache行

🔬 Cache的写策略详解

写策略是Cache设计中最复杂的部分:

写策略决策树: 写操作 │ ├─ 写命中 (Write Hit) │ ├─ Write-Through: 同时写Cache和内存 │ │ 优点: 数据一致性好, 实现简单 │ │ 缺点: 每次写都访存, 带宽消耗大 │ │ │ └─ Write-Back: 只写Cache, 标记Dirty │ 优点: 减少内存访问, 带宽省 │ 缺点: 需要Dirty位, 替换时写回 │ └─ 写缺失 (Write Miss) ├─ Write-Allocate: 先读入Cache, 再写 │ 优点: 后续写可能命中 │ 缺点: 首次写缺失需要读内存 │ └─ No-Write-Allocate: 直接写内存 优点: 首次写缺失快速 缺点: 后续写仍然缺失 常见组合: Write-Through + No-Write-Allocate: 简单, 适合I/O Write-Back + Write-Allocate: 高效, 适合通用计算 本课实现: Write-Back + Write-Allocate