“沿用访存队列的单线程整数槽、独立地址/数据就绪、唯一load版本、纯总仿射表达式与顺序退休。推测查询仍在已知同址的较老store中选年龄最大者,仍要等待它的数据;改变的只是:未知地址的老st…”
一条load的地址已经算出来,缓存里也有这个位置的旧值,就能马上返回吗?还不行:前面可能有一条尚未写入内存的store,而它才是程序要求读取的来源。更麻烦的是,那条store也许连地址都还没算出。访存队列要回答的不是“缓存命中了吗”,而是“这次读究竟应该接哪次写”。
形式陈述
地址、数据、提交分别记录
采用顺序退休的单线程有限指令窗口。动态项按程序年龄编号0、1、2、……;年龄越小越老,不能用重复出现的PC代替动态身份。store项保存地址就绪位、数据就绪位及对应值;load项保存地址、执行结果与来源。store只有到退休队首且所需内容齐全时才改架构内存M,准备好数据不等于已经写出。
本页把内存建模为“整数槽名→整数值”,未写过的槽初值0。一个槽是不可拆分的抽象位置,槽名可以为负;它不是字节地址,也不暗中实施对齐与跨界访问。没有其他线程、DMA、设备、异常、地址转换、缓存缺失或部分字节覆盖。
每个load产生独立的结果版本。后续地址或store数据可以是常量,也可以是
一个查询的三个出口
设当前load年龄为
- 只要有一个较老store地址未知,返回WAIT及这些动态年龄
- 否则找出地址等于x的较老store,若非空,取年龄最大的
- 若
的数据未知,返回WAIT;数据已知则返回该值,并记来源 - 若没有同址较老store,返回M[x],来源记为−1
公式中的“最近”是程序年龄最近:
集合为空时才读M。不能选最早完成者,也不能因为最新匹配项的数据没来,就改用更老的现成数据。完整参考器的 select_source 直接给出这个纯查询接口;WAIT不改状态。
这个版本有意保守:即使一个已知的较年轻同址store可能遮住未知老写,第一步仍等待全部老地址。这样规则容易审计,但不是等待最少的实现。下一页会明确改变这一个许可条件,而不是在这里偷偷放宽。
直觉
两个“还没好”不是一回事
store的地址未知,意味着“它可能改的就是我要读的位置”。store地址已经是9、load要读4,那么它的数据即使还没到,也不妨碍这次读4。只有地址确实匹配且是最近那条写时,数据未到才构成直接等待。
BOOM官方实现文档也把store地址生成与数据到达分开:地址早到可以更早澄清依赖,数据晚到则可能让匹配load休眠等待。本文只提取这种分工,采用自己的整数槽与事件协议,不把官方完整RVWMO机器的全部行为搬进来。[1]
转发则让“尚未提交的正确值”提前服务年轻消费者。store仍留在窗口,架构内存可以继续保存旧值3,而后面的load已经读到队列里的17。这没有破坏退休前缀,因为load结果也是暂定版本,尚未成为已提交观察。
例子与边界
有两个同址写时,17并不是可用答案
M[4]=3,load年龄5,地址4。窗口中有:
| store年龄 | 地址 | 数据 | 说明 |
|---|---|---|---|
| 0 | 4 | 17 | 较老同址写 |
| 2 | 4 | 未到 | 最近同址写 |
| 4 | 9 | 未到 | 已知不同址 |
全部地址已知,所以不是“未知地址”等待。但年龄2的数据未到,正确结果仍是WAIT。年龄0已经有17,M还有3,都不能代替年龄2将来要写的值。等年龄2数据到23,load从2转发23;年龄4的数据仍可未到。
如果年龄2随后退休,它把23写进M并离开未退休store集合。此后load从M读到23,数值仍正确;来源由队列项变成已提交内存,是生命周期的变化,不是数据丢失。
把年龄0的地址改成未知,即使年龄2仍已知为4且数据23,本页规则也返回WAIT。允许这个load先读23需要新的证明:一旦年龄0稍后也揭晓为4,年龄2仍处于它与load之间,才构成遮蔽。内存依赖重放会用记录的来源年龄检查这件事。
四条指令把转发接到最终内存
初值M[4]=3、M[8]=0,执行:
I0: store [4] := 17 // 地址到第6轮才揭晓,数据第1轮就绪
I1: v1 := load [4]
I2: store [8] := 2*v1+1
I3: v3 := load [8]
PC分别为0x100、0x104、0x108、0x10c。其余地址、数据释放延迟均为1。这里“延迟”是自动驱动器让该字段最早可计算的调度轮数,不是load/cache的实际硬件周期。
保守模式在第1轮知道I1、I2、I3的地址,也知道I0的数据17,但I0地址未知。I1不能读内存3;I3也不能因为I2数据未到就绕过去读0。第6轮I0地址揭晓为4,I1从I0转发17。第7轮I2才能用这个结果形成35,I3再从I2转发35。
退休分别发生在第7、8、9、10轮,最终M[4]=17、M[8]=35,已提交load结果为17与35。保守模式没有重放,两次load各执行一次。先有正确来源,随后才谈等待是否值得优化。
共同调度轮怎么数
下载驱动器初始把有限程序全部放入窗口,并按固定阶段运行:
| 每轮阶段 | 本轮允许的动作 |
|---|---|
| 退休 | 至多一个已经DONE的队首 |
| 地址 | 按年龄计算所有已到释放时刻且源值已到的未知地址 |
| 数据 | 按年龄计算所有具备同样条件的store数据 |
| store完成 | 按年龄把地址/数据齐全且预测前驱已完成的store标DONE |
| load | 按选定年龄策略扫描,至多成功执行一条load |
地址、数据阶段不设硬件端口上限;这是可复算的教学驱动,不是吞吐量模型。默认选最老可执行load,也提供最年轻优先迁移。刚在load阶段产生的结果,要到下一轮地址/数据阶段才供消费者使用。恢复后的项从恢复轮重新起算释放延迟。
推论与应用
最近写为什么等于顺序值
先假设当前字段来自正确的更老load结果,且已提交M等于退休前缀。考虑load在程序中的全部更老store。它们恰分成两组:退休前缀中的写已按序进入M,其余仍在窗口。
若未退休组没有同址写,那么顺序执行到load之前对x的最后一次写一定已经在M中;读M正确。若未退休组存在同址写,年龄最大的
保守的未知地址等待保证没有漏掉一个其实更接近load的匹配项。数据等待保证没有以更老值代替最终来源。对程序年龄归纳,更老load正确便使后续地址和数据表达式正确,进而使本条load正确。结合队首退休,已提交内存和load结果始终对应顺序前缀。
这个论证离不开“没有外部写者”和“整槽访问”。混合宽度访问可能需要逐字节从多个store拼接;其他核心的写会引入一致性与coherence约束。本文的一个最大年龄不能直接处理那些问题。
扫描器花费在哪里
设本次查询涉及q个较老store,M显式保存m个槽。数学上的最近写选择可一遍扫描,用 O(q+1) 次字比较和 O(1) 工作空间;若要输出全部未知项,另需 O(q) 输出空间。
公开纯接口还校验内存字典中的全部键值,并把未知年龄排序以稳定输出,因此实际最坏时间为
硬件的关联比较可以并行做,却需比较器、选择电路和连线;“一个硬件拍完成”与“软件常数时间”没有相互推出关系。终点先要求选对来源,再比较推测和恢复如何改变实际完成的工作。
参考资料
- Berkeley Out-of-Order Machine,The Load/Store Unit,官方文档,访问于2026-10-10,Store Instructions、Store Micro-Ops、Load Instructions。用于地址/数据分开就绪、队列转发和提交边界背景;本文固定单核整数槽与保守地址等待。
- George Z. Chrysos、Joel S. Emer,Memory Dependence Prediction using Store Sets,ISCA1998,pp.142–153,§§1、3.1及脚注1。原文无推测基线还等待store数据,本文明确允许绕过已知不同址但数据未到的store。