当前 O3 前端里的 FTQ 的定位是:

  1. BAC(Branch Address Calculation) 生成 FetchTarget 后,用于在 BACFetch 之间传递取指目标的队列。
  2. FetchTarget 的承载体,用来描述一段连续的 fetch 范围,以及该范围退出点的预测信息。
  3. 一个临时托管 BPU PredictorHistory 的结构,用于桥接“预测发生时刻”与“预译码拿到真实 branch/seqNum 时刻”之间的时间差。

当前实现中:

  • BACFTQ 的生产者。
  • FetchFTQ 的消费者。
  • BPU 是预测与恢复逻辑的真正拥有者。
  • FTQFetchTarget 和尚未正式入账的 PredictorHistory 的过渡驻留点。

与 FTQ 直接相关的文件是:

  • ftq.hh/cc:定义 FetchTargetFTQ
  • bac.hh/cc:负责生成 FetchTarget、把它放入 FTQ、在 fetch 阶段把历史迁回 BPU。
  • fetch.cc:负责消费 FTQ 头部的 FetchTarget

前端组织方式

BAC 被拆成两部分

BAC 的职责:

  1. Branch prediction / FTQ feeding 部分
    • 在 decoupled frontend 模式下作为一个独立活动阶段运行。
    • 负责利用 BTB 扫描地址流、发现可能的 branch、调用 BPU 产生预测,并构造 FetchTarget 插入 FTQ
  2. 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 扫描地址流,先生成 FetchTarget
  • Fetch 后续只按 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: 线程 id
  • is_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

于是实现采用一个过渡策略:

  1. 在 BAC 生成 FT 并调用 bpu->predict(...) 时,先拿到一份 PredictorHistory
  2. 这份历史不立即放进 BPU 的 predHist 正式队列
  3. 它先挂到 ft->bpuHistory 上,在 FTQ 中临时保存
  4. 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
  • 如果 FTQInvalid 或队列为空,则返回 nullptr

isHeadReady():

  • 只有在队列非空且不为 Invalid 时才为真
  • Fetch 在 decoupled 模式下会依赖这个条件来判断能否继续取指

popHead():

  • 消费当前 FTQ 头项
  • 如果头项仍然携带 bpuHistory,则弹出失败,并将 FTQ 置为 Invalid

任意 FetchTarget 在被真正弹出 FTQ 之前,挂在它上面的 bpuHistory 必须已经被转移走,或者在 squash 路径中被清空

squash():

  • 清空队列
  • 但它内部假设所有 FetchTarget 上的 bpuHistory 已经先被外部清掉
  • 因此正常顺序是:
    1. BAC 先调用 squashBpuHistories() 倒序回滚历史
    2. 再调用 ftq->squash() 清队列

FTQ 填充:BAC 生成 FetchTarget

输入:BAC 维护自己的前端 PC

在 decoupled frontend 中,BAC 维护自己的 bacPC
(不是 Fetch 正在使用的 fetch PC,而是“预测生产侧”的 PC)

  • Fetch 消费 FTQ 中已有的 fetch targets
  • BAC 则在后台持续尝试生成后续 fetch targets

搜索 branch

BAC::generateFetchTargets() 的基本策略是:

  1. 从当前 bacPC 开始
  2. 逐地址检查 BTB 是否命中
  3. 如果未命中,则继续向前扫描,直到:
    • 找到 branch,或
    • 扫描宽度达到 fetchTargetWidth

BAC 并不依赖预译码来识别 branch,而是纯粹依赖 BTB 来发现 branch

  • 只有进入过 BTB 的 branch,BAC 才能在生成 FT 时看到
  • never-taken branch、首次遇到的 branch、被 BTB 驱逐的 branch,都可能在 BAC 阶段完全不可见

预测 branch

一旦 BTB 命中:

  1. BAC 通过 BTBGetInst() 取出对应的 StaticInst
  2. 调用 BAC::predict()
  3. BAC::predict() 内部进一步调用 bpu->predict(...)
  4. BPU 返回预测方向,同时更新传入的 pc 为预测目标或 fallthrough
  5. BPU 生成的 PredictorHistory 不进入 BPU 正式历史队列,而是直接挂到 ft->bpuHistory

完成 FetchTarget

无论是否发现 branch,BAC 都会调用 curFT->finalize(...),设置:

  • endPC
  • is_branch
  • taken
  • predPC

随后将该 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

流程是:

  1. ft->bpuHistory 取出
  2. 给这份 history 填入真实 seqNum
  3. 调用 bpu->insertPredictorHistory(...) 插入 BPU 主历史队列
  4. 按 history 中记录的预测结果更新 fetch PC

情况 B:FT 上没有对应 history

这意味着:

  • BAC 生成 FT 时没发现这个 branch
  • 常见原因是 BTB miss、首次出现、从未 taken、或者 BTB entry 被替换

此时采用一个保守恢复机制:

  1. 新建一个 PredictorHistory
  2. 调用 bpu->branchPlaceholder(...) 构造 predictor-specific 的占位历史
  3. 假设该 branch 的预测是 not-taken
  4. 先把这份 dummy history 插入 BPU 正式历史体系
  5. 如果后续 decode/commit 发现真实情况不是这样,再依赖 squash 路径纠正

为什么不能在 BAC 预测时直接插入 BPU 主历史?

  1. 当时没有真实 seqNum
    • BPU 主历史以动态指令序号管理 in-flight branch
  2. 当时 branch 类型信息可能不够稳定
    • 特别是复杂 instruction / micro-op 场景下,单纯依赖 BTB 中保存的静态信息可能还不够安全
  3. 需要允许“预测已发生,但尚未被真正消费”
    • FTQ 本质上就是为了支持这种 decoupling

FT 消费完成的处理

如果满足以下任一条件,当前 ft 指针会被置空:

  • 当前 instruction 是 FT 的 exit instruction,且若是 micro-op 则必须是最后一个 micro-op
  • FTQ 已变为 not ready

一旦当前 FT 被置空,Fetch 就尝试:

  1. ftq->popHead()
  2. 若成功,读取下一个 FT
  3. 若失败,则认为状态异常并触发 bacResteer()

popHead 为什么可能失败?

头部 FetchTarget 上仍残留 bpuHistory

这意味着某个关键步骤没有按预期发生,例如:

  • exit branch 的 history 没有在 updatePreDecode() 中成功取走
  • 或者发生了复杂 corner case,导致 FT 被消费结束,但对应分支历史还没有完成迁移

当前实现对这种情况的处理非常保守:

  • 不尝试在 popHead() 时“补救性处理”历史
  • 而是直接把 FTQ 置 Invalid,让外层走 squash/resteer 恢复

squash, mispredict, resteer 时的恢复流程

恢复顺序

  1. 先回滚 FTQ 中尚未正式进入 BPU 主历史队列的历史
  2. 再清空 FTQ
  3. 再根据 decode/commit/fetch 反馈,对 BPU 正式历史进行 squash/update
  4. 重设 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

则会:

  1. 先回滚 FTQ 中现有悬挂历史
  2. 将 FTQ lock
  3. 对当前 instruction 重新做一次新预测

这表明当前实现并不试图为复杂 instruction 提供完美的细粒度多 branch FT 建模,而是通过锁定和重建来保守处理

优点:

  • 实现简单
  • 一致性风险较低

代价:

  • 会频繁走 lock -> squash/resteer 路径
  • 不适合用来精确模拟非常激进、能在复杂 macro-op 内做高精度 branch streaming 的工业前端