Cache一致性 — Cache Coherence (MESI Protocol)

多核系统的数据一致性:MESI协议状态机

📖 多核Cache一致性问题

单核系统中Cache对软件透明。但多核系统中,每个核心有自己的私有Cache,同一内存地址在不同Cache中可能有不同的副本和不同的值

Cache一致性问题: Core 0 Cache: Core 1 Cache: ┌──────────────┐ ┌──────────────┐ │ Addr 0x100: 42│ │ Addr 0x100: 7│ ← 不一致! └──────────────┘ └──────────────┘ 时序: 1. Core 0 读 0x100 → 7 (Cache: 0x100=7) 2. Core 1 读 0x100 → 7 (Cache: 0x100=7) 3. Core 0 写 0x100 = 42 (Cache0: 0x100=42, Cache1: 0x100=7 ❌) 一致性要求: ┌───────────────────────────────────────────┐ │ 单写多读: 同一地址,只有一个Cache可以写 │ │ 写传播: 写操作必须被所有Cache看到 │ │ 写串行化: 所有Core看到相同的写顺序 │ └───────────────────────────────────────────┘
协议状态数代表特点
MSI3简单多核最基础,无Exclusive状态
MESI4x86, ARM增加Exclusive,减少总线流量
MOESI5AMD, ARM增加Owner,优化共享脏行
MOESIF6Intel增加Forward,优化广播

🔬 MESI协议状态机详解

MESI四个状态: M (Modified) — 脏:只有本Cache有,且被修改过,与主存不一致 E (Exclusive) — 独占:只有本Cache有,但与主存一致 S (Shared) — 共享:多个Cache有,与主存一致 I (Invalid) — 无效:本Cache没有该行 状态转换 (总线侦听 + 本地读写): 本地读: I → E (总线读,无其他Cache响应) ← 独占获取 I → S (总线读,有其他Cache响应) ← 共享获取 S → S (命中) E → E (命中) M → M (命中) 本地写: I → M (总线写,使其他Cache无效) ← 写分配 S → M (总线无效化 + 升级) ← 升级 E → M (无需总线操作!) ← 关键优化 M → M (命中) 远程读 (侦听): M → S (提供数据给请求者,写回主存) E → S (提供数据给请求者) 远程写 (侦听): M → I (提供数据 + 写回,然后失效) E → I (失效) S → I (失效)
E状态的关键作用:如果Core独占一个Cache行(只有它有副本),写该行时不需要发总线消息,直接M→E→M。这大大减少了多核系统的总线流量。MSI协议没有E状态,每次独占写都需要广播,性能较差。

🖥️ Verilog实现:MESI Cache控制器

// Lesson 38: MESI Cache Controller (simplified per-line)
module mesi_cache #(
    parameter ADDR_W = 32, DATA_W = 32, ID = 0
)(
    input  wire                clk, rst_n,
    input  wire [ADDR_W-1:0]  cpu_addr_i,
    input  wire [DATA_W-1:0]  cpu_wdata_i,
    input  wire                cpu_we_i, cpu_req_i,
    output reg  [DATA_W-1:0]  cpu_rdata_o,
    output reg                 cpu_hit_o,
    // Bus snooping interface
    input  wire                bus_grant_i,
    input  wire [DATA_W-1:0]  bus_data_i,
    input  wire [ADDR_W-1:0]  bus_addr_i,
    input  wire                bus_valid_i, bus_we_i,
    output reg  [ADDR_W-1:0]  bus_addr_o,
    output reg  [DATA_W-1:0]  bus_data_o,
    output reg                 bus_req_o, bus_we_o,
    output reg                 bus_valid_o
);
    localparam M=2'd3, E=2'd2, S=2'd1, I=2'd0;
    reg [1:0] state;
    reg [DATA_W-1:0] line_data;
    reg [ADDR_W-1:0] line_tag;
    reg              line_valid;
    wire tag_match = line_valid && (line_tag == cpu_addr_i);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state <= I; line_data <= 0;
            line_tag <= 0; line_valid <= 0;
            bus_req_o <= 0; bus_valid_o <= 0;
        end else begin
            bus_req_o <= 0; bus_valid_o <= 0;
            bus_we_o <= 0; cpu_hit_o <= 0;

            // Snoop: 远程写 → 失效
            if (bus_valid_i && bus_we_i && line_valid
                && bus_addr_i == line_tag) begin
                if (state == M) begin
                    bus_req_o <= 1; bus_data_o <= line_data;
                end
                state <= I;
            end
            // Snoop: 远程读 → 降级
            else if (bus_valid_i && !bus_we_i && line_valid
                     && bus_addr_i == line_tag) begin
                if (state == M) begin
                    bus_req_o <= 1; bus_data_o <= line_data;
                    state <= S;
                end else if (state == E) state <= S;
            end
            // 本地访问
            else if (cpu_req_i) begin
                if (tag_match) begin  // 命中
                    cpu_hit_o <= 1; cpu_rdata_o <= line_data;
                    if (cpu_we_i) begin
                        line_data <= cpu_wdata_i;
                        state <= M;
                    end
                end else begin  // 未命中
                    cpu_hit_o <= 0;
                    if (bus_grant_i) begin
                        bus_req_o <= 1; bus_valid_o <= 1;
                        bus_addr_o <= cpu_addr_i;
                        if (cpu_we_i) begin
                            bus_we_o <= 1;
                            line_data <= cpu_wdata_i;
                            state <= M;
                        end
                    end
                    if (bus_grant_i && !cpu_we_i && bus_valid_i) begin
                        line_data <= bus_data_i;
                        line_tag <= cpu_addr_i;
                        line_valid <= 1;
                        cpu_rdata_o <= bus_data_i;
                        cpu_hit_o <= 1;
                        state <= E;
                    end
                end
            end
        end
    end
endmodule
Verilator仿真验证通过 — MESI协议状态转换正确,M/E/S/I四状态验证

📊 MESI协议性能特征

操作总线消息延迟频率
E状态写(独占写)01周期~30%
S→M升级写1 (Invalidate)~5周期~15%
I→M写未命中2 (Read + Invalidate)~20周期~10%
M→S共享降级1 (Write-back)~30周期~5%
MESI协议的性能优化核心是减少总线消息:E状态避免了独占写时的广播。在典型多核负载中,约30%的写操作可以E→M无总线完成。

🧪 实验练习

  1. 实现完整MESI总线仲裁器:多Cache竞争时的优先级仲裁
  2. 添加Owner状态(MOESI):共享脏行优化
  3. 测量总线流量:统计不同工作负载下的Invalidate/RFO消息数
  4. 实现Directory协议:集中式目录替代总线侦听
MESI协议验证
思考题:MESI的"写传播"如何保证?当Core 0写一个S状态的行时,其他Core的S状态行如何被无效化?如果总线消息延迟不同,如何保证所有Core看到相同的写顺序?
参考资料:Hennessy & Patterson §5.3 | Intel MESI Protocol | ARM AMBA ACE Spec