多级缓存与写入

读 miss 只需要把缺失的 block 带到更近的位置。写访问多了一层约束:处理器已经修改了一份副本,下层存储何时看见这个修改?若替换时还有未写回的数据,旧 block 能否直接丢弃?这些问题决定了写策略、dirty bit 和多级缓存的平均访问时间。

本篇的核心问题是:write-through、write-back、write-allocate 和 no-write-allocate 分别改变哪条流量;脏块逐出怎样计入代价;AMAT 的条件概率公式何时能用?核心案例用 1 set × 2 way × 16B 的 write-back/write-allocate cache 追踪五次访问,并补一个 no-write-allocate 写 miss 的反例。证据等级为功能执行加手算。

实验目录为 examples/computer-architecture/cache/。本文附件包括 writeback_amat.json.txtcache_model.py.txtrun_cases.py.txt

四个写策略词不要混成一个选择

write-through 表示写命中时同时写 cache 和下层。write-back 表示先只改 cache line,把 dirty bit 置上,等这行被替换时再写回下层。

write-allocate 表示写 miss 时先把 block 装入 cache,再在 cache 中完成写。no-write-allocate 表示写 miss 不装入 cache,只把这次写转给下层。

因此常见组合有两类:write-through + no-write-allocate,适合避免一次写 miss 把很少再读的数据带进 cache;write-back + write-allocate,适合多次写同一 block 时合并下层流量。但这只是常见组合,不是逻辑必然。本模型显式覆盖了 write_back=True, write_allocate=False 的写 miss,证明即使不分配 line,这次 4B 写流量也不能消失。

写策略要拆成两个维度:命中时是否立刻下传,缺失时是否装入 cache。write-back 描述 dirty line 何时写回;write-allocate 描述 write miss 是否分配 cache line。二者独立,不能用一个词替代另一个。

五次访问的脏块轨迹

主案例固定操作序列:

1
read 0, write 0, write 16, read 32, read 0

cache 参数为 1 set × 2 way × 16B,write-back + write-allocate,hit latency 为 1,fill latency 为 10。所有 block 都映射到同一个 set。逐次结果如下:

次序 操作 block 结果 cache 内变化 下层流量
1 read 0 0 miss 装入 block 0,clean fill 16B
2 write 0 0 hit block 0 置 dirty 0B
3 write 16 1 miss 装入 block 1,dirty fill 16B
4 read 32 2 miss 淘汰 dirty block 0,装入 block 2 fill 16B + writeback 16B
5 read 0 0 miss 淘汰 dirty block 1,装入 block 0 fill 16B + writeback 16B

模型输出为 fill_bytes=64writeback_bytes=32writebacks=2。注意第 2 次写命中没有立刻增加下层写流量,代价推迟到第 4 次逐出。write-back 不是减少语义上的写入次数,而是把多次写合并成更少的下层 block 写回。

第 5 次重新读取地址 0 时 miss,是容量缺失而不是冲突缺失。这个 cache 只有两行,访问过且仍可能需要的 block 有 0、1、2。即使改成同容量全相联,也只有两行。

no-write-allocate 的最小反例

另一个反例使用单独 cache:1 set × 1 way × 16B,write_back=True, write_allocate=False。第一次访问就是 write 0

模型事件为:fill_bytes=0lower_write_bytes=4。这说明 no-write-allocate 没有装入 16B block,但 4B store 仍要向下层发出。否则写操作就在模型里凭空消失。

这个反例的作用是防止把“write-back”误理解成“所有写都等到未来替换”。write-back 描述 cache line 已经存在且被写脏后的处理;no-write-allocate 的写 miss 没有 line 可脏化,只能把本次写交给下层。

AMAT 是概率树,不是所有延迟的总公式

单级 AMAT 写作:

1
AMAT = hit_time + miss_rate × miss_penalty

两级缓存常写成递归形式:

1
AMAT = L1_hit + L1_miss_rate × (L2_hit + L2_local_miss_rate × memory_penalty)

本文手算参数为 L1_hit=1L1_miss_rate=0.25L2_hit=8L2_local_miss_rate=0.5memory_penalty=80。代入得:

1
2
3
1 + 0.25 × (8 + 0.5 × 80)
= 1 + 0.25 × 48
= 13 cycles

这里的 L2_local_miss_rate 是“已经 miss L1 的访问里,又 miss L2 的比例”,不是全部访存的 L2 miss 比例。CS61C 的 AMAT 教材也按这个条件概率口径展开两级公式。

AMAT 适用于平均、独立、无重叠的访问延迟估算。它不描述 MSHR 重叠、乱序隐藏、预取提前、写缓冲排队或 DRAM bank 并行。若后续模型引入 MLP,同一个 miss penalty 不再简单逐项相加。

AMAT 是一棵概率树。每一层的 miss rate 必须说明分母,公式才能读。L2 local miss rate 的分母是已经到达 L2 的访问;若使用全局 miss rate,公式需要相应改写。

脏块逐出与平均时间的边界

本案例把 dirty eviction 的额外时间粗略写成一个 fill latency。这样做只用于展示“写回也占用下层带宽和时间”,不代表真实内存控制器的读写切换、写缓冲吸收或总线仲裁。

若写缓冲足够深,处理器看到的等待可能小于实际写回完成时间;若写缓冲满了,后续 miss 可能被迫等待。若 cache 层级 inclusive 或需要维护一致性,dirty line 的位置还会影响后续协议消息。本文不覆盖这些机制。

验收时不应只看最后 AMAT=13。更重要的是能指出:第 4、5 次访问为什么各有一次 dirty eviction;为什么 no-write-allocate 仍有 4B 下层写;为什么 L2 miss rate 的分母是 L1 misses。

复跑与验收

执行:

1
bash examples/computer-architecture/cache/scripts/run_all.sh

本篇关注 outputs/writeback_amat.json。应核对主案例 fills=4fill_bytes=64writebacks=2writeback_bytes=32;AMAT 为 13.0;no-write-allocate 反例中 fill_bytes=0lower_write_bytes=4

这些记录证明的是教学模型中的写策略和计数口径。没有测量真实 cache 的 writeback 事件,也没有证明某款处理器采用同一写策略。

两道练习

练习一: 若第 3 次 write 16 改为 write-through + write-allocate,写 miss 装入 block 后是否还需要向下层写 4B?

解答:需要。write-allocate 决定装入 block;write-through 决定写命中或装入后的写同时下传。因此这次既有 16B fill,也有 4B lower write。具体系统可能合并或重排总线事务,但语义流量不能写成 0。

练习二: 某资料给出 L1 miss rate 10%,L2 global miss rate 2%,L1 hit 1 cycle,L2 hit 8 cycles,memory penalty 100 cycles。若公式需要 L2 local miss rate,应填多少?AMAT 是多少?

解答:L2 global miss rate 是全部访问中 miss L2 的比例,L1 miss rate 是进入 L2 的比例。local miss rate 为 0.02 / 0.10 = 0.2。AMAT 为 1 + 0.10 × (8 + 0.2 × 100) = 3.8 cycles

参考资料