LSQ 与内存依赖重放

寄存器重命名可以在译码附近确定每个源值对应的生产者,内存访问更难:两条不同形式的地址表达式可能在运行时指向同一位置。年轻 load 若绕过了一个尚未算出地址的较老 store,可能读到旧值;即使它所读的缓存行已经命中,程序结果仍然会错。

本篇的核心问题是:store 的数据先准备好、地址稍后才确定时,load 应等待,还是可以先推测执行?核心案例比较两种策略,要求最终结果与同一个顺序程序一致。例子还包含两个较老 store,避免把“找到一个同地址写”误当成“找到正确的写”。

证据等级为教学时序模型。实验限定单线程、四字节、自然对齐访问,没有缓存、地址翻译、多核、原子操作或部分字节重叠。实际事件见 lsq.json.txt,独立可执行模型见 lsq_model.py.txt

store 有地址和数据两组依赖

一条 sw rs2,offset(rs1) 需要从 rs1 计算有效地址,也需要 rs2 提供待写数据。这两项可能分别依赖不同的较老指令。知道“要写 22”并不能确定应该阻塞哪一个 load;知道“要写地址 128”但数据仍未产生,则已经足以判断某个 load 是否必须等待。

BOOM 的 LSU 文档将 store 的地址与数据有效状态分开,说明地址和数据可以分别送到 store queue。本例让数据先于地址就绪,是为了暴露地址未决造成的内存依赖问题,不是在声称所有程序都具有这种时序。

LSQ(Load/Store Queue)通常包含 load queue 和 store queue。对于本篇问题,store 条目至少需要程序年龄、地址及其有效位、数据及其有效位;load 条目需要地址、是否执行、得到的值,以及推测所绕过的较老 store 信息。只保留“load 已完成”这一位,无法在地址随后解决时判断旧结果是否有效。

顺序语义先确定正确答案

初始内存 mem[128]=7。程序顺序为:

1
2
3
S0: store 11 -> [128]
S1: store 22 -> [unknown until t=4]
L2: load [128]

主案例中 S1 最终地址也是 128。按程序顺序执行后,L2 必须得到 22。S0 与 S1 都比 L2 老,但 S1 是距离 L2 最近的一次同地址写,因此 11 已被覆盖。

模型固定如下事件:

条目 最终地址 数据 地址就绪 数据就绪
S0 128 11 t=0 t=0
S1 128 22 t=4 t=1
L2 128 由访存决定 t=0 不适用

表中列出最终地址是为了让读者能检查答案;调度器在 address_ready 之前不能使用这个字段的值。教学实现通过显式时刻判断屏蔽尚未可用的信息。如果预测策略偷看“未来已知的地址”,就不再是地址推测实验。

所有 store 在这个有限实验中都保留在队列内,没有在 load 验证之前排空到内存。load 因而可能直接从 store queue 取得数据。这种 store-to-load forwarding 与第 10 篇的寄存器前递都需要选择正确生产者,但匹配依据变成了运行时内存地址。

转发必须选择最近的较老 store

设 load 的程序序号为 L。同地址候选集合应满足三个条件:store 序号小于 L,地址已经确定,而且与 load 地址匹配。在本篇等宽对齐前提下,候选中的最大序号给出 load 应读取的版本:

1
2
3
source = max_age({S | S.age < L.age
and S.address_known
and S.address == L.address})

如果这个最近候选的数据未就绪,load 必须等它,不能转向更老但数据已就绪的 store。否则会读到已被顺序程序覆盖的版本。若没有匹配候选,可以读内存,但仍须处理地址未知的较老 store 带来的不确定性。

本模型输入只包含比唯一 load 老的 store,序号检查确保条目按年龄排序;没有实现动态分配 load 序号。这个限制让选择规则可以逐项观察,也说明附件不能直接当作通用 LSQ 使用。

地址完全相等的比较只在当前边界内足够。例如两字节 store 与四字节 load 可以部分重叠,届时需要按字节合并或等待,不能继续用单一整数值转发。本模型显式拒绝宽度二、地址 130 的四字节访问,以及负地址,不将这些情况静默当成不相关。

保守等待:等未知地址变成已知

保守策略只在所有较老 store 的地址都已解决后执行 load。t=0 时 S0 已可转发 11,但 S1 地址未知,所以 L2 等待。t=1 仅新增 S1 数据=22,并未消除地址依赖。t=4 S1 地址确定为 128,L2 选择较年轻的 S1,读取 22。

实际输出中的关键事件为:

1
2
3
4
t=1  S1.data_ready
t=4 S1.address_ready
t=4 L2.execute source=S1 value=22 speculative=false
t=4 L2.validated value=22

这个模型的读取与转发没有额外延迟,所以执行和验证可以在 t=4 同时发生。若改成需要一个周期的转发或缓存访问,应增加真实事件与状态,而不只把统计结果加一。

保守策略没有错误推测,也不会重复执行这个 load;代价是可能等待一个最终与它地址不同的 store。它提供了正确性基线,但是否更快需要结合实际冲突概率、恢复代价和能够重叠的其他工作判断。

推测执行:先取已知版本,再检查晚到地址

推测策略允许 load 绕过地址未决的较老 store。t=0,S0 的地址和数据已经就绪,L2 从 S0 转发 11,并把结果标记为 speculative。这个标签表示它还不是可提交的确定结果;S1 的地址仍可能推翻它。

t=4,S1 的地址被解析为 128。S1 比 L2 老,又比 L2 原先使用的 S0 年轻,因此 L2 忽略了一次必须覆盖旧值的写。模型撤销 11,并从 S1 重新执行 load:

1
2
3
4
5
t=0  L2.execute  source=S0 value=11 speculative=true
t=4 S1.address_ready address=128
t=4 L2.replay discarded_value=11
t=4 L2.execute source=S1 value=22 speculative=false
t=4 L2.validated value=22

实际记录 replays=1,最终结果仍是 22。故障发现与重放过程不能省略为一句“最后会纠正”;如果年轻计算已经使用过 11,还需要取消并重做那些受影响结果。本附件只有一个 load,没有模拟它的年轻消费者,因此只验证 load 自身的撤销和再执行。

BOOM LSU 文档的 Memory Ordering Failures 部分描述了 store 地址解决后检测年轻 load 的错误执行,并通过恢复处理内存顺序推测失败。其完整流水线恢复范围比这个单 load 模型更大,不能把本例的一步重放代价当成 BOOM 或真实处理器的处罚周期。

已使用的较年轻写可以遮蔽更老写

检测条件不能简化为“任何晚到同地址 store 都导致重放”。假设某个 load 已经从序号 5 的较老 store 转发,而随后才解析出序号 3 的 store 同样写这个地址。只要二者都比 load 老,且没有部分重叠等额外情况,序号 3 的值已经被序号 5 覆盖,不应替代 load 的来源。

本模型记录 source_store。新解析 store 必须比当前来源更年轻,才可能推翻那个值;如果 load 原先读的是内存,则来源使用哨兵 -1,任何匹配的较老 store 都可能成为遗漏的写。这个年龄判断与“寻找最近较老生产者”的规则一致。

这项推导限定等宽单地址访问。附件只执行 S0→S1 的覆盖案例,没有穷举所有年龄排列,因而不构成形式证明。扩充输入时应同时检查最终值和是否发生必要的重放。

非别名与数据晚到的对照

回归把 S1 的最终地址改为 132,其他时序不变。推测 load 在 t=0 仍从 S0 得到 11;t=4 发现 S1 与地址 128 不重叠,不发生重放,最终验证结果为 11。

这个对照没有证明四周期的程序加速。本模型直到 t=4 才验证 load,也没有模拟能够提前使用 11 的消费者,因而只证明了“更早产生一个后来被证实正确的推测值”。要报告程序耗时收益,需要加入后续依赖工作、提交边界和资源约束。

另一个对照让唯一较老 store 在 t=1 算出地址 128,数据到 t=4 才成为 33。此时地址依赖早已明确,load 仍需等待数据,最终 t=4 才执行。它防止实现把 address_ready 误当成整个 store 已 ready。

情形 首次执行值 重放次数 最终值 可以得到的结论
主案例保守等待 22 0 22 未解决地址阻塞 load
主案例地址推测 11 1 22 冲突检测和重放恢复结果
S1 改写地址 132 11 0 11 非别名时旧推测得到验证
地址早、数据晚 33 0 33 匹配来源未有数据仍要等待

与 ROB 和多核内存模型的连接

ROB 限制架构提交,年轻 load 不能越过尚未解决的较老异常生效。LSQ 则检查 load 是否读到了程序顺序要求的内存版本。按序提交本身不能修复一个已经读错值、却被错误标记为正常完成的 load;LSQ 必须把推测失败反馈给恢复机制。

单线程中同地址的转发与重放,也不足以说明跨核访问顺序。store buffer 何时排空、其他核何时可见、不同地址访问允许怎样观察,属于一致性和内存模型问题。本篇没有 RVWMO 或 C/C++ 原子语义实验,不用普通共享变量竞争来演示这些结论。

复跑与验收

累计回归命令为:

1
PYTHONDONTWRITEBYTECODE=1 python3 examples/computer-architecture/ooo/src/run_all.py

附件也可以执行:

1
python3 lsq_model.py.txt

检查 outputs/lsq.json:保守和推测主案例都等于顺序结果 22;推测记录中确实出现一次丢弃 11 的 replay;非别名对照为 11 且无重放;数据晚到案例在 t=4 执行;三个非法输入均被拒绝。这里的验证范围包括结果、时间事件和拒绝边界,不包括缓存命中率或真机延迟。

两道练习

练习一: 两个较老 store 都写地址 128,S0 数据已就绪,较年轻的 S1 地址已知但数据未就绪。load 可否先取 S0 的值,并把它标记为已经验证?

解答:不能。S1 是顺序程序中覆盖 S0 的最近写;地址已经确认相同,结果不是“不确定但可能正确”,而是明确缺少应读取的数据。模型必须等待 S1 的数据,不能把 S0 当成可提交来源。

练习二: 保留主案例时序,把 S1 最终地址改为 132,能否只依据 load 在 t=0 得到 11,就宣称比保守策略快四周期?

解答:不能。地址未决到 t=4 才消除,当前模型的最终验证时刻没有提前;也没有年轻消费者、队列压力或完整提交时间。可以确认推测值更早可用且无需重放,完整性能收益需要增加相应模型和测量区间。

参考资料

  • BOOM:The Load/Store Unit。2026-09-19 核查在线版;Store Instructions、Load Instructions、Memory Ordering Failures 支持地址与数据分离、转发和错误推测恢复。
  • BOOM:The Reorder Buffer。Commit Stage 与 Exceptions and Flushes 支持 store 获准提交和内存顺序推测失败的恢复接口。
  • 第 13 篇:ROB 与精确异常。对比“结果算出”和“允许成为架构状态”的条件。