L10:高级访存操作(Advanced Memory Operations)
MIT 6.5900 Fall 2024 · Joel Emer 主题:写性能与流水化写、写策略、读缺失代价、推测 load/store 与存储缓冲、内存依赖预测(Store Sets)、预取
一、写性能
直接映射 Cache 的读:tag 检查与数据读可并行。但写呢?
写在访存级要两个周期:一个周期做 tag 检查,命中后再一个周期写数据——完全串行!
降低写命中时间
把"命中/缺失"视为对微架构值的数据依赖。解法:
- 停顿——尽快交付数据:电路级技术(CAM tag)或许能一周期写;
- 推测命中 + 急切更新数据:设计能在一周期内同时读写的数据 RAM,tag 缺失(abort)后恢复旧值;
- 推测缺失 + 惰性更新数据:把 store 写数据暂存在 Cache 前的单个缓冲,在下一个空闲数据访问周期写入 Cache(commit)。
流水化/延迟写
问题:需要提交惰性保存的写数据。解法:在下一个 store 做 tag 检查时的空闲数据周期写入数据。
流水化写:若指令需要延迟写缓冲中的数据怎么办?→ 旁路。旁路条件:tag 与 index 都匹配"延迟写地址"。
二、写策略选择
命中时:
- 写穿透(Write-through):同时写 Cache 与内存——流量大但简化多处理器设计;
- 写回(Write-back):只写 Cache(逐出时才写内存)——每块一个脏位可进一步减流量。
缺失时:
- 不写分配(No-write-allocate):只写主存;
- 写分配(Write-allocate / fetch on write):取入 Cache。
常见组合:写穿透 + 不写分配、写回 + 写分配。
降低读缺失代价
问题:写缓冲可能持有读缺失所需位置的更新值(RAW 数据冒险)。
- 停顿:读缺失时等写缓冲排空;
- 旁路:把写缓冲地址与读缺失地址比较,无匹配则让读缺失先行,有匹配则返回写缓冲中的值。
写缓冲持有:写回 Cache 的逐出脏行,或写穿透 Cache 的所有写。
三、推测 load / store
寄存器依赖已用 tag/物理寄存器号处理,那么内存操作呢?
问题:与寄存器更新一样,store 在指令提交前不应永久改变体系结构内存状态。
- 数据更新策略:急切还是惰性?→ 惰性:加推测存储缓冲(speculative store buffer)惰性持有推测 store 数据;
- store-to-load 数据冒险:停顿/旁路/推测?→ 旁路。
存储缓冲的职责
- 惰性存数据:缓冲 store 的新数据值;
- 提交/中止:最老指令的数据须提交到内存或被遗忘;
- 旁路:在较老 store 提交前,把其数据转发给较年轻的指令。
提交一般按序进行——为什么?WAW 冒险。
存储缓冲——惰性数据管理
- store 执行时:标记 valid + speculative,存 tag、data、指令号;
- store 提交时:清 speculative 位,最终把数据移入 Cache;
- store 中止时:清 valid 位。
存储缓冲——旁路
- 数据同时在存储缓冲与 Cache 中用哪个?→ 存储缓冲(若 store 比 load 老);
- 同地址在存储缓冲出现两次用哪个?→ 比 load 老的最年轻 store;
- 旁路需检查的字段:Valid、Inum、tag;
- 计算所需存储缓冲项可视为对索引的依赖,可用简单预测器推测;猜错则声明误推测并中止。
四、内存依赖
对寄存器用 tag/物理寄存器号判依赖,内存操作呢?
sw x1, (x2)
lw x3, (x4)load 何时依赖 store?——当 x2 == x4 时。ROB 在发射时知道吗?→ 不知道。
1. 按序内存队列(朴素停顿)
按程序序执行所有 load/store ⇒ load/store 须等之前所有 load/store 执行完才能开始(仍可相对其他指令推测、乱序执行)。
2. 保守乱序 load 执行(智能停顿)
把 store 拆成两阶段:地址计算与数据写。
- 若地址已知且
x4 != x2,可在 store 前执行 load; - 每个 load 地址与之前所有未提交 store 的地址比较(可用部分保守检查,如地址低 12 位);
- 若有任何之前 store 地址未知,则不执行该 load(MIPS R10K,16 项地址队列)。
3. 地址推测(Address Speculation)
- 猜
x4 != x2,在 store 地址已知前执行 load; - 若确实
x4 != x2则提交; - 若
x4 == x2则压制(squash)load 及其后所有指令。
为支持 squash,需按程序序保留所有已完成但未提交的 load/store 地址与数据。如何检测需 squash?——监视在需要其数据的 load 之后到达的 store。
推测 load 缓冲(Speculative Load Buffer)
- load 执行时:标记 valid,设指令号与地址 tag;提交/中止时清 valid;
- 推测检查:检测某 load 是否在对同地址的较早 store 之前执行了(漏掉的 RAW 冒险);
- store 地址到来时,若 load 缓冲中有指令号比该 store 年轻的项 → 推测违例,中止!
tag 匹配不必完美(部分匹配即可);不准确的地址推测代价大 → 用预测降低误推测几率。
五、内存依赖预测
简单方案(Alpha 21264)
- 猜
x4 != x2,在 store 前执行 load; - 若后来发现
x4 == x2,squash load 及其后指令,并把该 load 标为 store-wait; - 该 load 指令的后续执行将等待所有之前 store 完成;周期性清除 store-wait 位。
注意预测器的通病:学到了却难以"忘掉"。
Store Sets(Alpha 21464)
针对多读者/多写者(多代码路径、单一位置的多个组成部分):
- 每个 load 必须等待其 store set 中尚未执行的 store;
- 处理器先允许朴素推测、记录内存序违例,从而近似每个 load 的 store set;
- Store Set Map Table:用 store/load 索引把引起内存序违例的 store/load 对关联起来,并支持多读者共享同一 store set。
六、预取(Prefetching)
load 的执行"依赖"其所需数据在 Cache 中——可推测未来的指令与数据访问并预取入 Cache(指令访问比数据访问更易预测)。
种类:硬件预取、软件预取、混合方案。
预取对缺失的影响:强制缺失减少,冲突/容量缺失约略增加。
预取的三个要点
- 有用性:应产生命中;
- 及时性:不太晚也不太早;
- Cache 与带宽污染。
硬件指令预取(Alpha 21064)
缺失时取两块:请求块 ii i 放入 Cache,下一块 i+1i{+}1 i+1 放入指令流缓冲(stream buffer);若 Cache 缺失但流缓冲命中,则把流缓冲块移入 Cache 并预取 i+2i{+}2 i+2。
硬件数据预取
- 缺失时预取:缺失 bb b 时预取 b+1b{+}1 b+1;
- 单块前瞻(OBL):访问块 bb b 时为 b+1b{+}1 b+1 发起预取(与"加倍块大小"不同);可扩展为 N 块前瞻(流预取);
- 步长预取(Strided):若观察到访问序列 b,b+N,b+2Nb, b{+}N, b{+}2N b,b+N,b+2N,则预取 b+3Nb{+}3N b+3N 等。
例:IBM Power 5(2003)每处理器支持 8 个独立步长预取流,预取到当前访问之前 12 行。
小结
- 写比读慢(tag 检查 + 数据写串行),用 CAM tag、推测命中急切更新或延迟写缓冲 + 旁路优化;写策略在流量与复杂度间权衡;
- 推测 load/store 用惰性存储缓冲保存推测数据,store-to-load 用旁路;提交按序以避 WAW;
- 内存依赖在发射时未知,用按序队列 → 保守乱序 → 地址推测 + 推测 load 缓冲逐步放开,并用 store-wait / Store Sets 预测内存依赖;
- 预取(硬件/软件)减少强制缺失,关键在有用性、及时性与污染控制。