Gem5: Fetch Target Queue(FTQ)
当前 O3 前端里的 FTQ 的定位是:
BAC(Branch Address Calculation) 生成FetchTarget后,用于在BAC与Fetch之间传递取指目标的队列。FetchTarget的承载体,用来描述一段连续的 fetch 范围,以及该范围退出点的预测信息。- 一个临时托管
BPU PredictorHistory的结构,用于桥接“预测发生时刻”与“预译码拿到真实 branch/seqNum 时刻”之间的时间差。
当前实现中:
BAC是FTQ的生产者。Fetch是FTQ的消费者。BPU是预测与恢复逻辑的真正拥有者。FTQ是FetchTarget和尚未正式入账的PredictorHistory的过渡驻留点。
与 FTQ 直接相关的文件是:
ftq.hh/cc:定义FetchTarget与FTQ。bac.hh/cc:负责生成FetchTarget、把它放入 FTQ、在 fetch 阶段把历史迁回 BPU。fetch.cc:负责消费 FTQ 头部的FetchTarget。
前端组织方式
BAC 被拆成两部分
BAC 的职责:
- Branch prediction / FTQ feeding 部分
- 在 decoupled frontend 模式下作为一个独立活动阶段运行。
- 负责利用
BTB扫描地址流、发现可能的 branch、调用 BPU 产生预测,并构造FetchTarget插入FTQ
- PC update / predecode 协调部分
- 与
Fetch紧耦合 Fetch每预译码一条指令,就调用BAC::updatePC()决定下一条 PC- 在 decoupled 模式下,真正把
FTQ里暂存的预测历史迁回 BPU,也发生在这里
- 与
源码注释也明确指出,这种组织方式并不完美,但目的是把 decoupling 逻辑主要收敛在
BAC内,而不是进一步复杂化Fetch
Coupled frontend 与 decoupled frontend 的差异
当前代码同时支持两种模式:
Coupled frontend
Fetch在预译码到 branch 时,直接调用bpu->predict(...)- 不需要
FTQ介入预测流程 - 预测和
Fetch更同步
Decoupled frontend
BAC提前基于BTB扫描地址流,先生成FetchTargetFetch后续只按 FTQ 第一项取指Fetch在预译码到 branch 后,不再重新做预测,而是从FTQ里取出已生成的预测历史
FTQ
FetchTarget
FetchTarget 可以近似理解成一个“前端预测驱动的连续取指块”。它和传统基本块 basic block 有相似性,但并不完全等价:
- 有一个
startPC和一个endPC - 可能包含
surprise branch: BAC 当时看不到、只能在后续 predecode 才发现的 branch
FetchTarget 包含以下信息:
startPC: fetch target 的起始地址endPC: fetch target 的结束地址,对应退出 instruction 的 PC。predPC- 如果退出 instruction 是 branch,则这是该退出 branch 对应的预测目标地址
- 如果退出 instruction 不是 branch,那么这个字段仍会在
finalize()时被填入“下一个 FT 的起点”
ftSeqNum:FetchTarget自己的编号,不是指令的seqNum(因为 FT 是在真正生成动态指令之前就构建出来的)tid: 线程 idis_branch: 退出 instruction 是否是 branch。taken: 退出 branch 是否预测为 taken。bpuHistory:BPredUnit::PredictorHistory *,用于在 FT 等待于 FTQ 期间暂存该 fetch target 对应的分支预测历史
FetchTarget 结束的条件:
- 找到 branch
- 达到最大搜索宽度
- 在复杂 micro-op 场景下触发特殊处理后被保守截断
为什么
bpuHistory需要在 FetchTarget 上?因为在 decoupled frontend 中,预测发生得更早,而真正动态指令的
seqNum更晚才出现
在 gem5 当前实现里:
- BPU 的正式历史管理以指令序号
seqNum为核心- 但 BAC 构造 FT 的时候,还没有真实的动态指令,拿不到最终
seqNum于是实现采用一个过渡策略:
- 在 BAC 生成 FT 并调用
bpu->predict(...)时,先拿到一份PredictorHistory- 这份历史不立即放进 BPU 的
predHist正式队列- 它先挂到
ft->bpuHistory上,在 FTQ 中临时保存- 等
Fetch真正预译码到那个 branch,拿到动态指令和真实seqNum后,再把这份历史转移回 BPU 主历史队列
FTQ 任务
FTQ 本身主要负责:
- 存放
FetchTarget - 提供头尾插入和弹出
- 提供一些状态控制接口,如
invalidate()、lock() - 在 squash 时清空队列
- 为外部逻辑提供遍历接口,以便 BAC 回滚 FTQ 中仍未正式转入 BPU 的历史
FTQ 状态:
Valid: 可以安全读取和消费队列Invalid- 当前队列中的 fetch target 不再可信
- 进入该状态后,需要依靠
resteer/squash来恢复
Locked- 头部 fetch target 仍然可用,但其后的项不再可信
- 常出现在复杂指令或多 branch micro-op 的保守处理路径中
FTQ 还有
Full的相关概念,但严格来说“是否已满”主要通过size >= numEntries计算,不是一个单独用于外部语义判断的常驻状态机值;BAC侧会据此进入FTQFull阻塞状态
FTQ API
insert():
- 将
FetchTarget压入队尾 - 由
BAC调用
readHead():
- 返回当前头部
FetchTarget - 如果
FTQ已Invalid或队列为空,则返回nullptr
isHeadReady():
- 只有在队列非空且不为
Invalid时才为真 Fetch在 decoupled 模式下会依赖这个条件来判断能否继续取指
popHead():
- 消费当前 FTQ 头项
- 如果头项仍然携带
bpuHistory,则弹出失败,并将 FTQ 置为Invalid
任意
FetchTarget在被真正弹出 FTQ 之前,挂在它上面的bpuHistory必须已经被转移走,或者在 squash 路径中被清空
squash():
- 清空队列
- 但它内部假设所有
FetchTarget上的bpuHistory已经先被外部清掉 - 因此正常顺序是:
- BAC 先调用
squashBpuHistories()倒序回滚历史 - 再调用
ftq->squash()清队列
- BAC 先调用
FTQ 填充:BAC 生成 FetchTarget
输入:BAC 维护自己的前端 PC
在 decoupled frontend 中,BAC 维护自己的 bacPC
(不是 Fetch 正在使用的 fetch PC,而是“预测生产侧”的 PC)
Fetch消费 FTQ 中已有的 fetch targetsBAC则在后台持续尝试生成后续 fetch targets
搜索 branch
BAC::generateFetchTargets() 的基本策略是:
- 从当前
bacPC开始 - 逐地址检查
BTB是否命中 - 如果未命中,则继续向前扫描,直到:
- 找到 branch,或
- 扫描宽度达到
fetchTargetWidth
BAC并不依赖预译码来识别 branch,而是纯粹依赖BTB来发现 branch
- 只有进入过
BTB的 branch,BAC 才能在生成 FT 时看到never-taken branch、首次遇到的 branch、被 BTB 驱逐的 branch,都可能在 BAC 阶段完全不可见
预测 branch
一旦 BTB 命中:
BAC通过BTBGetInst()取出对应的StaticInst- 调用
BAC::predict() BAC::predict()内部进一步调用bpu->predict(...)- BPU 返回预测方向,同时更新传入的
pc为预测目标或 fallthrough - BPU 生成的
PredictorHistory不进入 BPU 正式历史队列,而是直接挂到ft->bpuHistory
完成 FetchTarget
无论是否发现 branch,BAC 都会调用 curFT->finalize(...),设置:
endPCis_branchtakenpredPC
随后将该 FT 插入 FTQ
确定下一 FT 的起点
- 如果当前 branch 预测为 taken:下一个 FT 的起点就是预测目标
predPC - 如果当前 branch 预测为 not-taken,或根本没有 branch: 下一个 FT 的起点就是顺序地址
FTQ 消费: Fetch 读取并 pop FetchTarget
Fetch 只处理头部 FT
Fetch 在 decoupled 模式下,首先会检查 FTQ 是否 ready:
- 若 FTQ 为空或无效,则
Fetch进入FtqWait - 若 FTQ 可用,则读取头部
FetchTarget
当前 PC 必须落在当前 FT 的范围内
Fetch 会验证当前 fetch PC 是否在 curFT->inRange(...) 范围内
如果不在范围内,说明前端状态与 FTQ 内容已经不同步,需要触发 bacResteer() 进行恢复
Fetch 在 FT 范围内顺序取指
只要当前 PC 仍然位于 curFT 内:
Fetch就继续从 I-cache / decoder 中取出指令- 每生成一条
DynInst,都调用bac->updatePC(instruction, next_pc, curFT)
Fetch 预译码到 branch 后,分支历史从 FTQ 转回 BPU
当 Fetch 发现当前 instruction 是 control instruction:
- 在 coupled 模式下,直接在这里调用
bpu->predict(...) - 在 decoupled 模式下,不再重新预测,而是调用
BAC::updatePreDecode(...)
updatePreDecode() 的处理:
情况 A:是当前 FT 的 exit branch,且 FT 上有 bpuHistory
这说明:
- BAC 之前已经对这个 branch 做过预测
- 现在到了真正的预译码阶段,需要把历史正式转入 BPU
流程是:
- 将
ft->bpuHistory取出 - 给这份 history 填入真实
seqNum - 调用
bpu->insertPredictorHistory(...)插入 BPU 主历史队列 - 按 history 中记录的预测结果更新
fetch PC
情况 B:FT 上没有对应 history
这意味着:
- BAC 生成 FT 时没发现这个 branch
- 常见原因是
BTB miss、首次出现、从未 taken、或者 BTB entry 被替换
此时采用一个保守恢复机制:
- 新建一个
PredictorHistory - 调用
bpu->branchPlaceholder(...)构造 predictor-specific 的占位历史 - 假设该 branch 的预测是 not-taken
- 先把这份 dummy history 插入 BPU 正式历史体系
- 如果后续 decode/commit 发现真实情况不是这样,再依赖 squash 路径纠正
为什么不能在 BAC 预测时直接插入 BPU 主历史?
- 当时没有真实
seqNum
- BPU 主历史以动态指令序号管理 in-flight branch
- 当时 branch 类型信息可能不够稳定
- 特别是复杂 instruction / micro-op 场景下,单纯依赖 BTB 中保存的静态信息可能还不够安全
- 需要允许“预测已发生,但尚未被真正消费”
- FTQ 本质上就是为了支持这种 decoupling
FT 消费完成的处理
如果满足以下任一条件,当前 ft 指针会被置空:
- 当前 instruction 是 FT 的 exit instruction,且若是 micro-op 则必须是最后一个 micro-op
- FTQ 已变为 not ready
一旦当前 FT 被置空,Fetch 就尝试:
ftq->popHead()- 若成功,读取下一个 FT
- 若失败,则认为状态异常并触发
bacResteer()
popHead为什么可能失败?头部
FetchTarget上仍残留bpuHistory这意味着某个关键步骤没有按预期发生,例如:
- exit branch 的 history 没有在
updatePreDecode()中成功取走- 或者发生了复杂 corner case,导致 FT 被消费结束,但对应分支历史还没有完成迁移
当前实现对这种情况的处理非常保守:
- 不尝试在
popHead()时“补救性处理”历史- 而是直接把 FTQ 置
Invalid,让外层走 squash/resteer 恢复
squash, mispredict, resteer 时的恢复流程
恢复顺序
- 先回滚 FTQ 中尚未正式进入 BPU 主历史队列的历史
- 再清空 FTQ
- 再根据 decode/commit/fetch 反馈,对 BPU 正式历史进行 squash/update
- 重设 BAC 的 PC,重新开始生成 FT
BAC::squashBpuHistories()
- 倒序遍历 FTQ
- 对每个仍带有
bpuHistory的 FT,调用bpu->squashHistory(...) - 清空对应
ft->bpuHistory
之所以倒序,是因为这些 history 代表的是按预测路径逐步累积的 speculative history update,恢复时必须反向回滚
不同来源的 squash
会从多个来源触发恢复:
commit: 例如真实 branch mispredict 在提交时被确认decode: 例如更早发现的控制流纠错fetch: 例如前端发现当前 FT 与实际 PC 不一致,或在消费 FT 过程中检测到异常
复杂指令与多 branch micro-op 的处理
最复杂的 corner case 是:
- 一个 instruction 含多个 branch 语义
- 或 branch 不在最后一个 micro-op
- 或 BTB 中保存的 branch 类型与最终预译码出的真实类型不一致
当前实现对这些情况采取的是非常保守的策略
branch type mismatch
如果从 FT 上取下来的 history 所记录的 branch 类型,与当前预译码得到的 branch 类型不同:
- 认为当前预测历史不可信
- 先把 history 放回 FT
- 立刻回滚 FTQ 中所有悬挂历史
- 将 FTQ 置
Locked
这样做的含义是:
- 不再信任当前队列后续项
- 等当前复杂 instruction 处理完,再通过 resteer 重建前端状态
多 branch complex instruction
对于包含多个 branch 的复杂 instruction,当前实现只在 BAC 生成 FT 时预测“第一个可见 branch”
如果在后续 predecode 发现:
- 当前 micro-op 并不是最后一个
- 但已经没有可用 history
则会:
- 先回滚 FTQ 中现有悬挂历史
- 将 FTQ
lock - 对当前 instruction 重新做一次新预测
这表明当前实现并不试图为复杂 instruction 提供完美的细粒度多 branch FT 建模,而是通过锁定和重建来保守处理
优点:
- 实现简单
- 一致性风险较低
代价:
- 会频繁走
lock -> squash/resteer路径 - 不适合用来精确模拟非常激进、能在复杂 macro-op 内做高精度 branch streaming 的工业前端