Skip to content

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

  1. x4 != x2,在 store 地址已知前执行 load;
  2. 若确实 x4 != x2 则提交;
  3. 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)

  1. x4 != x2,在 store 前执行 load;
  2. 若后来发现 x4 == x2,squash load 及其后指令,并把该 load 标为 store-wait
  3. 该 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 预测内存依赖;
  • 预取(硬件/软件)减少强制缺失,关键在有用性、及时性与污染控制。