Skip to content

算法Algorithm

访存队列与最近写转发

Load-store queue forwarding · LSQ forwarding · 访存队列转发

把store地址与数据的就绪分开记录,在较老写中选择最近同址来源,并用等待条件保持load与单线程顺序语义一致。

一条load的地址已经算出来,缓存里也有这个位置的旧值,就能马上返回吗?还不行:前面可能有一条尚未写入内存的store,而它才是程序要求读取的来源。更麻烦的是,那条store也许连地址都还没算出。访存队列要回答的不是“缓存命中了吗”,而是“这次读究竟应该接哪次写”。

形式陈述 ​

地址、数据、提交分别记录 ​

采用顺序退休的单线程有限指令窗口。动态项按程序年龄编号0、1、2、……;年龄越小越老,不能用重复出现的PC代替动态身份。store项保存地址就绪位、数据就绪位及对应值;load项保存地址、执行结果与来源。store只有到退休队首且所需内容齐全时才改架构内存M,准备好数据不等于已经写出。

本页把内存建模为“整数槽名→整数值”,未写过的槽初值0。一个槽是不可拆分的抽象位置,槽名可以为负;它不是字节地址,也不暗中实施对齐与跨界访问。没有其他线程、DMA、设备、异常、地址转换、缓存缺失或部分字节覆盖。

每个load产生独立的结果版本。后续地址或store数据可以是常量,也可以是 avj+b,其中 vj 来自更老load,a、b为数学整数。操作数没到便不能计算表达式;一旦到齐,计算纯粹、确定、总定义。这个小接口已经能表现“错读一个值,年轻store的地址和数据一起算错”,无须另造一套完整寄存器指令集。

一个查询的三个出口 ​

设当前load年龄为 ℓ、地址为x,只检查未退休且年龄小于 ℓ 的store。已退休的写已包含在M中;年轻store不允许给老load转发。保守查询按以下顺序执行:

  1. 只要有一个较老store地址未知,返回WAIT及这些动态年龄
  2. 否则找出地址等于x的较老store,若非空,取年龄最大的 s∗
  3. 若 s∗ 的数据未知,返回WAIT;数据已知则返回该值,并记来源 s∗
  4. 若没有同址较老store,返回M[x],来源记为−1

公式中的“最近”是程序年龄最近:

s∗=max{s:s<ℓ, s 尚未退休且地址为 x}.

集合为空时才读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,执行:

text
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正确。若未退休组存在同址写,年龄最大的 s∗ 晚于全部已退休写,也晚于窗口里其他同址写,所以它就是顺序语义要求的最后写者。等其数据就绪后转发,得到同一个值。

保守的未知地址等待保证没有漏掉一个其实更接近load的匹配项。数据等待保证没有以更老值代替最终来源。对程序年龄归纳,更老load正确便使后续地址和数据表达式正确,进而使本条load正确。结合队首退休,已提交内存和load结果始终对应顺序前缀。

这个论证离不开“没有外部写者”和“整槽访问”。混合宽度访问可能需要逐字节从多个store拼接;其他核心的写会引入一致性与coherence约束。本文的一个最大年龄不能直接处理那些问题。

扫描器花费在哪里 ​

设本次查询涉及q个较老store,M显式保存m个槽。数学上的最近写选择可一遍扫描,用 O(q+1) 次字比较和 O(1) 工作空间;若要输出全部未知项,另需 O(q) 输出空间。

公开纯接口还校验内存字典中的全部键值,并把未知年龄排序以稳定输出,因此实际最坏时间为 O(m+qlog⁡(q+1)+1),临时列表占 O(q)。这是Python参考器的真实账单,不把诊断格式整理当免费。核心窗口可以在建表时验证内存,保持store年龄顺序并避免排序,得到线性扫描版本;本文没有把该优化冒充已实施。

硬件的关联比较可以并行做,却需比较器、选择电路和连线;“一个硬件拍完成”与“软件常数时间”没有相互推出关系。终点先要求选对来源,再比较推测和恢复如何改变实际完成的工作。

参考资料
  1. Berkeley Out-of-Order Machine,The Load/Store Unit,官方文档,访问于2026-10-10,Store Instructions、Store Micro-Ops、Load Instructions。用于地址/数据分开就绪、队列转发和提交边界背景;本文固定单核整数槽与保守地址等待。
  2. George Z. Chrysos、Joel S. Emer,Memory Dependence Prediction using Store Sets,ISCA1998,pp.142–153,§§1、3.1及脚注1。原文无推测基线还等待store数据,本文明确允许绕过已知不同址但数据未到的store。
关系图谱5 个相邻概念 · 1 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组