0%

现有处理器的 Cache 一致性实现

Xuantie C910

Reference:

  1. open-c910 github 仓库
  2. 玄铁C910微架构学习(15)– 缓存一致性结构及其实现

Xuantie C910/C920 为两级 cache 的结构,其中 L1 Cache 私有, L2 Cache 多核共享,并采用 MOESI 协议维护多个处理器核心的 cache 一致性。

Xuantie C910 的标量部分开源到了 open-c910 github 仓库,且其通过 AMBA ACE 总线实现 MOESI 协议

处理器核发出的请求事务

l1icache / ifu

in ifu/rtl/ct_ifu_ipb.v

l1icache miss 或者 prefetch 时向总线发起请求

1
2
3
4
5
6
7
8
9
10
11
12
13
assign pref_tsize = 1'b1;
...
assign ipb_tsize = (ref_req_for_biu) ? l1_refill_ipb_tsize
: pref_tsize;
...
//Prepare snoop request
assign ipb_page_share = !cp0_ifu_insde;
assign ipb_share_refill = ipb_tsize && ipb_page_share;
assign ipb_rd_domain[1:0] = (!ipb_cacheable) ? 2'b11
: {1'b0, ipb_page_share};

assign ipb_rd_snoop[3:0] = (ipb_share_refill)? 4'b1 //ReadShared
: 4'b0;//ReadNoSnoop
  • 如果 Cacheable && Cache Enable (ipb_tsize) 和请求地址所在的页是共享的(ipb_page_share),则发出 ReadShared 事务
  • 反之则发出 ReadNoSnoop 事务

l1dcache / lsu

l1dcache: Read Buffer

l1dcache miss 时 lsu 中的 read buffer 需要向总线发起请求获取数据 refill l1dcache

in lsu/rtl/ct_lsu_rb.v

1
2
3
4
5
6
7
8
9
...
rb_biu_ar_snoop[3:0] = 4'b0;
casez({rb_atomic_readunique,rb_biu_share_refill,rb_biu_req_st})
3'b1??:rb_biu_ar_snoop[3:0] = 4'b0111;//ReadUnique
3'b011:rb_biu_ar_snoop[3:0] = 4'b0111;//ReadUnique
3'b010:rb_biu_ar_snoop[3:0] = 4'b0001;//ReadShared
default:rb_biu_ar_snoop[3:0] = 4'b0;//ReadNoSnoop & ReadOnce
endcase
...
  1. 原子指令的读请求(rb_atomic_readunique): ReadUnique 事务
  2. 请求地址 cacheable 和 shareable,起因是 store 指令引起的 cache miss: ReadUnique 事务
    • 将其它核的私有缓存中对应的缓存行无效
  3. 请求地址 cacheable 和 shareable,起因是 load 指令(!rb_biu_req_st)引起的 cache miss: ReadShared 事务
  4. 请求地址非 cacheable : ReadNoSnoop事务
  5. 请求地址 cacheable 和 sharable,但是该处理器核的缓存不使能: ReadOnce 事务
    • 读取的数据不能放入缓存中

l1dcache: Prefetch Unit

lsu 触发时预取也需要向总线发出请求:

in lsu/rtl/ct_lsu_pfu.v

1
2
3
assign pfu_biu_ar_snoop[3:0]  = pfu_biu_req_page_share
? 4'b0001 //ReadShared
: 4'b0000;//ReadNoSnoop
  • 如果请求地址所在的页是共享的(pfu_biu_req_page_share),则发出 ReadShared 事务
  • 否则发出 ReadNoSnoop 事务

l1dcache: Victim Buffer

dcache 的 linefill buffer 写回替换时需要通过总线向下一级写入踢出的 cacheline, C910 的 l1dcache 在写回时采用了 victim buffer 的设计,因此对总线的写入由 victim buffer 负责:

in lsu/rtl/ct_lsu_vb.v

1
2
3
4
5
6
7
8
9
10
11
12
...
vb_biu_aw_req_snoop[2:0] = 3'b0;
casez({vb_biu_aw_req_dirty,vb_biu_aw_req_lfb_create,vb_biu_aw_req_inv})
3'b01?:vb_biu_aw_req_snoop[2:0] = 3'b101;//writeevict
3'b11?:vb_biu_aw_req_snoop[2:0] = 3'b011;//writeback
3'b101:vb_biu_aw_req_snoop[2:0] = 3'b011;//writeback
3'b100:vb_biu_aw_req_snoop[2:0] = 3'b010;//writeclean
default:vb_biu_aw_req_snoop[2:0] = 3'b0;
...
assign vb_biu_aw_snoop[2:0] = vb_data_biu_req
? vb_biu_aw_req_snoop[2:0]
: 3'b100; //evict
  1. 要替换写回的 cache line 如果不为脏(!vb_biu_aw_req_dirty)则向总线发起 writeevict 事务
    • 为什么还需要写入?
  2. 要替换写回的 cache line 如果为脏(vb_biu_aw_req_dirty)则向总线发起 writeback 事务
  3. 如果是清除脏位且无效缓存行的操作,则发起writeback事务将脏的缓存行写回下一级内存中。
  4. 如果是清除脏位但是可以保留缓存行的副本,则发起writeclean事务将脏的缓存行写入下一级内存中。

l1dcache: Write Memory Buffer

dcache 的 Write Memory Buffer 负责将数据写回缓存或者下一级缓存

in lsu/rtl/ct_lsu_wmb.v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
wmb_biu_ar_snoop[3:0] = 4'b1011;
casez({wmb_read_req_atomic,wmb_read_req_icc,wmb_read_req_inst_type[1:0],wmb_read_req_inst_size[1:0]})
{1'b0,1'b1,2'b10,2'b01}:wmb_biu_ar_snoop[3:0] = 4'b1000;//CleanShared
{1'b0,1'b1,2'b10,2'b00}:wmb_biu_ar_snoop[3:0] = 4'b1000;//CleanShared
{1'b0,1'b1,2'b10,2'b10}:wmb_biu_ar_snoop[3:0] = 4'b1101;//MakeInvalid
{1'b0,1'b1,2'b10,2'b11}:wmb_biu_ar_snoop[3:0] = 4'b1001;//CleanInvalid
{1'b0,1'b1,2'b00,2'b??}:wmb_biu_ar_snoop[3:0] = 4'b1111;//CTC
{1'b0,1'b1,2'b01,2'b??}:wmb_biu_ar_snoop[3:0] = 4'b1111;
{1'b0,1'b1,2'b11,2'b??}:wmb_biu_ar_snoop[3:0] = 4'b1111;
default:wmb_biu_ar_snoop[3:0] = 4'b1011;//CleanUnique
endcase
...

assign wmb_biu_aw_snoop[2:0] = wmb_write_req_page_ca && wmb_write_req_no_wns && wmb_write_biu_dcache_line
? 3'b001 //WriteLineUnique
: 3'b000;//WriteNoSnoop or WriteUnique or Barrier
  1. cache 指令通过 ar_snoop 发送一致性请求
    • 对于清除脏位的 cache 指令(``),发出 cleanshared 事务
      将脏的缓存行写回下一级缓存
    • 对于无效某个缓存行的 cache 指令,发出 makeinvalid 事务,将该缓存行无效
    • 对于清除脏位且无效的 cache 指令,发出 cleaninvalid 事务
    • 对于无效 tlb 或者 icache 的指令,发出 DVM 事务
  2. 对于普通的读请求,ar_snoop 发出 cleanunique 事务
    将其它核中该 cacheline 副本的脏位清除并无效
  3. 对于写操作,通过 aw_snoop 发送一致性请求
    • 请求地址页面属性是 cacheable 且请求写整个 cacheline,发起 WriteLineUnique 事务
      传到其它核时会将该事务会被当作 MakeInvaild 事务处理,直接将 cacheline 的副本删除
    • 请求地址页面属性是 uncacheabel 则发起 WriteNoSnoop 事务
    • 请求写部分 cacheline,则发起 WriteUnique 事务
      传到其它核时会将该事务当作 CleanInvalid 处理,将脏的 cache 写回后将副本删除
    • 如果是内存屏障指令,如 fence 指令,则发起 Barrier 事务

l1dcache snoop 总线仲裁:

  1. AR 通道仲裁优先级: wmb(Write Memory Buffer) > rb(Read Buffer) > pfu(Prefetch Unit)

in lsu/rtl/ct_lsu_bus_arb.v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//priority: WMB > RB > pfu
...
assign bus_arb_wmb_ar_sel = bus_arb_wmb_ar_dp_req_real;
...
assign bus_arb_rb_ar_sel = !bus_arb_wmb_ar_dp_req_real
&& bus_arb_rb_ar_dp_req_real;
...
assign bus_arb_pfu_ar_sel = !bus_arb_wmb_ar_dp_req_real
&& !bus_arb_rb_ar_dp_req_real
&& bus_arb_pfu_ar_dp_req_real;
...
assign lsu_biu_ar_snoop[3:0] = {4{bus_arb_wmb_ar_sel}} & wmb_biu_ar_snoop[3:0]
| {4{bus_arb_rb_ar_sel}} & rb_biu_ar_snoop[3:0]
| {4{bus_arb_pfu_ar_sel}} & pfu_biu_ar_snoop[3:0];
  1. AW 通道仲裁优先级:vb(Victim Buffer) > wmb(Write Memory Buffer)

in biu/rtl/ct_biu_write_channel.v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
always @(...)
begin
if(cur_waddr_vict_awvalid)
begin
...
cur_waddr_buf_awsnoop[2:0] = cur_waddr_vict_awsnoop[2:0];
...
end
else
begin
...
cur_waddr_buf_awsnoop[2:0] = cur_waddr_st_awsnoop[2:0];
...
end
end

l1icache 与 l1dcache 之间的 snoop 总线请求会在 AR 通道上存在仲裁:

in biu/rtl/ct_biu_req_arbiter.v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
always @(...)
begin
if(!lsu_biu_ar_dp_req)
begin
...
arsnoop[3:0] = ifu_biu_rd_snoop[3:0]; //ReadNoSnoop
ardomain[1:0] = ifu_biu_rd_domain[1:0];//non-shareable domain
arbar[1:0] = 2'b0;
...
end
else
begin
...
arsnoop[3:0] = lsu_biu_ar_snoop[3:0];
ardomain[1:0] = lsu_biu_ar_domain[1:0];
arbar[1:0] = lsu_biu_ar_bar[1:0];
...
end
end

处理器核对请求事务的响应处理

  • C910 的 L1 Cache 采用 MESI 协议来维护缓存一致性
  • L2 Cache 采用 MOESI 协议维护缓存一致性

L1 Cache

  • 对于 DVM 事务使用 ctc queue 进行处理
  • 对于其它事务使用 snoop queue 处理

当接收到来自 AC 通道的请求后,仲裁器根据请求的事务类型(即 snoop 信号)将请求分派给 ctc queue / snoop queue.

in lsu/rtl/ct_lsu_snoop_req_arbiter.v

1
2
3
4
assign biu_lsu_snp_req  =   biu_lsu_ac_req
&& !biu_lsu_ctc_req;
assign biu_lsu_ctc_req = biu_lsu_ac_req
&& biu_lsu_ac_snoop[3:0] == 4'b1111; //ctc req

DVM 事务: ctc queue

  • 对于 DVM 事务,从 AC 通道传来的 snoop 信号为 4‘b1111,由 AC 通道的 addr 传递具体的请求
  • DVM 事务主要包括 tlb 和 icache 的无效请求

DVM 事务请求类型从 ac_addr 中解析得到:

Addr[14:12] Addr[6:5] Addr[0] Req Type
000 00 0 TLBI_ALL
000 00 1 TLBI_VA_ALL
000 01 0 TLBI_ASID_ALL
000 01 1 TLBI_VA_ASID
010 11 1 ICI_VA
010 00 0 ICI_ALL
  • 对于 TLBI_VA_ALL, TLBI_VA_ASID, ICI_VA 这三种(ac_addr[0] == 1)需要根据完整的地址来进行无效操作的请求
    • AC 通道需要两次传输,第二次传输负责传输地址信息
    • 对于 TLBI_VA 请求,第二次传输的是虚拟地址
    • 对于 ICI_VA 请求,第二次传输的是物理地址
  • 对于 TLBI_ASID_ALL 和 TLBI_VA_ASID 这两种(ac_addr[6:5] == 01)需要根据进程号无效 TLB 的请求
    • 第一次传输时 ac_addr[23:16]为 ASID 进程号
  • 对于 ICI_VA 需要根据 VA 和 PA 无效 icache 的请求
    • 第一次传输时 ac_addr[PA_WIDTH-1:16]为 VA
    • 第二次传输时 ac_addr为 PA

in lsu/rtl/ct_lsu_snoop_req_arbiter.v

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//==========================================================
// CTCQ entry content create
//1.Req type:first transaction
// | Addr[14:12] | Addr[6:5] | Addr[0] | |
// | 000 | 00 | 0 | TLBI_ALL |
// | 000 | 00 | 1 | TLBI_VA_ALL |
// | 000 | 01 | 0 | TLBI_ASID_ALL |
// | 000 | 01 | 1 | TLBI_VA_ASID |
// | 010 | 11 | 1 | ICI_VA |
// | 010 | 00 | 0 | ICI_ALL |
//2.ASID(for TLBI) or VA(for ICI):first transaction
// if(TLBI_ASID_ALL || TLBI_VA_ASID): ASID = Addr[23:16]
// if(ICI_VA): VA = ADDR[`PA_WIDTH-1:16]
//3.VA(for TLBI) or PA(for ICI):second transaction
//(only exist when first transaction addr[0]==1)
//
//==========================================================

assign arb_ctcq_ctc_type[5:0] = {biu_lsu_ac_addr[14:12] //1st trans
,biu_lsu_ac_addr[6:5]
,biu_lsu_ac_addr[0]};
assign arb_ctcq_ctc_asid_va[`PA_WIDTH-17:0] = biu_lsu_ac_addr[`PA_WIDTH-1:16]; //1st trans
assign arb_ctcq_ctc_2nd_trans = biu_lsu_ac_addr[0]; //1st trans
assign arb_ctcq_ctc_va_pa[`PA_WIDTH-5:0] = biu_lsu_ac_addr[`PA_WIDTH-1:4]; //2nd trans
  • ctc queue 共有 6 项,即最多可以接受 6 个 DVM 请求
  • 当接受请求后,根据请求的类型将无效请求和请求地址发送到 MMU 或者 icache 中进行无效化操作
  • MMU / ICACHE 在无效化处理完成后,会发出完成信号通知 ctc queue
    • 当该请求是最旧的请求时(连同 snoop queue 中的请求一起排序),则通过 CR 通道给出响应

其余事务: snoop queue

snoop queue 的任务:负责将互连网络传来的消息(通过 AC 通道)进行处理

  • 访问 dcache(or victim buffer / linefill buffer) 来判断请求的 cacheline 的状态
  • 根据请求事务的类型,更新 cacheline 的状态
  • 对于需要获取 cacheline 数据的事务,将访问 dcache 读取数据后,通过 CR 和 CD 通道做出响应

snoop queue 共有 6 项,即最多可以接受 6 个 snoop 请求

snoop queue entry:

  • snp_vld: valid
  • snp_addr: AC 通道传输的地址信息
  • snp_type: AC 通道传输的 snoop 信息,表示请求事务类型
  • snq_depd:表明 snp_addr 是否与 victim buffer(vb) 和 write memory buffer(wmb) 中的地址存在依赖关系
    • 如果存在依赖: 需要等待 vb 处理完成或者 wmb 写回完成后,snoop queue 才能开始进行处理
  • snq_way:请求的 cacheline 位于 cache 中的哪一路
  • snq_resp:将要通过 CR 通道做出的响应: cr_resp

snoop queue 与 victim cache / linefill buffer 的 bypass:

  1. victim buffer: 当 snoop queue entry 请求的地址与 victim buffer 中存在的数据地址相同时,如果 snoop queue entry 需要响应返回数据,则可以直接旁路 victim buffer 中的数据
  2. linefill buffer:当 linefill buffer 存在的地址与 snoop queue entry 请求地址相同时:
    • 如果需要更改缓存行的状态,则可以通过旁路直接给到 linefill buffer, 由 linefill buffer 之后写回缓存中
    • 由于 linefill buffer 中的数据为 refill cache 的数据,此时它的状态为 E 态 / S 态,更新状态时只可能是将它从 E 态 $\rightarrow$ S 态 / I 态
snoop 状态机
---
title: Snoop 状态机
---
stateDiagram
    _DEFAULT              --> SNPT_IDLE
    SNPT_IDLE             --> SNPT_WAIT_RESP

    SNPT_WAIT_RESP        --> SNPT_IDLE
    SNPT_WAIT_RESP        --> SNPT_WAIT_DATA_BUFFER
    SNPT_WAIT_RESP        --> SNPT_WAIT_POP
    SNPT_WAIT_RESP        --> SNPT_SDT_REQ

    SNPT_WAIT_DATA_BUFFER --> SNPT_WAIT_POP

    SNPT_WAIT_POP         --> SNPT_IDLE

    SNPT_SDT_REQ          --> SNPT_WAIT_SDT_CMPLT
    SNPT_SDT_REQ          --> SNPT_WAIT_POP

    SNPT_WAIT_SDT_CMPLT   --> SNPT_WAIT_DATA_BUFFER
    SNPT_WAIT_SDT_CMPLT   --> SNPT_WAIT_POP
  • SNPT_IDLE: 初始状态
    • 当 snoop entry 与 vb 和 wmb 没有依赖关系时,可以借用 store 流水线判断 cacheline 位于哪一路,将该 cacheline 的 share 位和 dirty 位读出
    • 当向 store 流水线发出请求后可以进入 SNPT_WAIT_RESP 状态等待响应
  • SNPT_WAIT_RESP: 在该状态下等待访问 dcache 的响应
    • 根据响应结果和请求的事务类型来判断是否需要读取 cacheline 数据或者改变 cacheline 的状态
    • 如果需要读取数据或改变状态则需要跳转到 SNPT_SDT_REQ
    • 如果不需要读取数据和改变缓存行状态,则判断此时与 victim buffer 是否还有依赖
      • 如果存在依赖: 进入 SNPT_WAIT_DATA_BUFFER 状态等待 victim buffer 处理完成
      • 否则进入 SNPT_WAIT_POP 向 CR 通道做出响应
  • SNPT_WAIT_DATA_BUFFER:等待 victim buffer 处理完成
    • 当 victim buffer 中不再有依赖时,跳转到 SNPT_WAIT_POP 向 CR 通道做出响应
  • SNPT_SDT_REQ:当需要读取 cacheline 数据或者改变 cacheline 状态时,会在该状态下等待当前 snoop 请求成为 snoop queue 中最老的一项时, 发出访问 dcache 的请求 (端口限制)
    • 如果 victim buffer / linefill buffer 中有 entry 匹配
      • victim buffer 匹配且不需要改变 cacheline 状态时: 跳转到 SNPT_WAIT_POP 向 CR 通道发出响应
      • linefill buffer 匹配: 跳转到 SNPT_WAIT_POP 向 CR 通道发出响应
    • 否则跳转到 SNPT_WAIT_SDT_CMPLT 等待请求完成
  • SNPT_SDT_CMPLT:该状态下等待访问 dcache 读取数据或者改变 cacheline 的状态
    • 当 dcache 仲裁器允许 snoop queue 访问时,则跳转到 SNPT_WAIT_POP 向 CR 和 CD 通道做出响应
  • SNPT_WAIT_POP:该状态下向 CR 和 CD 通道做出响应,等待总线接受响应
    • 当 CR 通道 ready 时(响应传输完成),即可跳转回初始状态 SNPT_WAIT_POP

访问 dcache 的响应:(in lsu/rtl/ct_lsu_snq_entry)

  • 响应来源主要有三个:dcache, linefill buffer 旁路, victim buffer 旁路
  • 主要的响应为三个位:valid, dirty, share
    1
    2
    3
    4
    5
    6
    7
    assign dcache_snq_tag_resp_valid_final = dcache_snq_tag_resp_valid || lfb_bypass_mode || vb_bypass_mode;
    assign dcache_snq_tag_resp_dirty_final = dcache_snq_tag_resp_valid
    && dcache_snq_tag_resp_dirty
    || vb_bypass_mode;
    assign dcache_snq_tag_resp_share_final = dcache_snq_tag_resp_valid
    ? dcache_snq_tag_resp_share
    : lfb_bypass_mode && snq_entry_bypass_dcache_share;
    • 在 dirty 位的响应中不需要考虑 linefill buffer 的旁路
      (在 linefill buffer 中的 cacheline 一定不处于 M 态)
    • 当 vb_bypass_mode 为高时,该 cacheline 的响应必定为脏
      (在 victim buffer 中的 cacheline 需要写回并且一定为脏)

判断是否需要读取 cacheline 数据的逻辑:
只有在 M 态下,且请求的事务需要读取或清除脏位( ReadShared / ReadUnique / ReadOnce / CleanInvalid / CleanShared) 时需要读取 cacheline 数据到 vb_sdb_data (同时充当 snoop queue 和 victim buffer 的 data buffer),当 snoop 进入 SNPT_WAIT_POP 状态后将数据通过 CD 通道返回给请求的核心或者写回下一级内存

判断是否需要改变 cacheline 状态:
根据访问 cache 得到的响应从而得到 cacheline 的状态,然后根据请求事务类型来判断是否需要改变状态:

Cacheline State 请求事务类型 状态迁移 迁移原因
M CleanShared / ReadShared M $\rightarrow$ S 别的核心需要共享该 cacheline (ReadShared),或者需要清除 cache 的脏位并保留副本(CleanShared)
E CleanShared, ReadShared E $\rightarrow$ S 其他核需要共享该 cacheline
M/E/S ReadUnique / CleanInvalid / MakeInvalid M/E/S $\rightarrow$ I 需要将该缓存行无效化
I - - -

通过 CR 通道的响应 snoop 请求:

  • WasUnique (C910 中不支持该信号因此恒为0)
  • IsShared: 代表在 snoop 过程完成后,该 cache 还保留着旧的副本
    • 对于 ReadShared 事务,snoop 过程后,该缓存行处于 S 态,保留着副本
    • 对于 ReadOnce 事务,不会改变 cacheline 的状态
    • 对于 CleanShared 事务,会清除 cacheline 的脏位,但是可以保留副本
  • PassDirty: 代表在 snoop 前 cacheline 为 M 态,且在 snoop 后需要将写回下一级内存的责任传递给发起 snoop 请求的核心或者互连网络
    • 除 MakeInvalid 外,其它会更改 M 态的事务(CleanShared / ReadShared / ReadUnique / CleanInvalid)都会将 pass_dirty 信号拉高
  • Error (C910 中不支持该信号因此恒为0)
  • DataTransfer: 代表是否需要传输数据 (同是否需要读取 cacheline 数据的判断)

in lsu/rtl/ct_lsu_snq_entry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
assign data_transfer =  (dcache_snq_tag_resp_valid_final && dcache_snq_tag_resp_dirty_final)    //(M) 
&& ( snq_type[3:0] == READ_SHARED
|| snq_type[3:0] == READ_UNIQUE
|| snq_type[3:0] == READ_ONCE
|| snq_type[3:0] == CLEAN_INVALID
|| snq_type[3:0] == CLEAN_SHARED);

assign pass_dirty = (dcache_snq_tag_resp_valid_final && dcache_snq_tag_resp_dirty_final) //(M)
&& ( snq_type[3:0] == READ_SHARED
|| snq_type[3:0] == READ_UNIQUE
|| snq_type[3:0] == CLEAN_INVALID
|| snq_type[3:0] == CLEAN_SHARED);

assign is_shared = dcache_snq_tag_resp_valid_final //
&& ( snq_type[3:0] == READ_SHARED
|| snq_type[3:0] == READ_ONCE
|| snq_type[3:0] == CLEAN_SHARED);
assign error = st_da_snq_ecc_err;
assign dcache_snq_resp[4:0] = {1'b0, //WasUnique donnot support
is_shared,
pass_dirty,
error, //error donnot support
data_transfer
};

L2 Cache

todo…