目录

  1. 引言
  2. 整体架构概览
  3. 流水线前端:Fetch / Decode / Rename
  4. 流水线后端:Issue / Execute / Writeback
  5. Commit 与精确异常
  6. 核心数据结构
  7. 分支预测机制
  8. Cache 层次交互
  9. 与 In-Order CPU 的对比
  10. 关键设计决策与总结

1. 引言

gem5 是一个广泛使用的计算机体系结构模拟器,被学术界和工业界用于处理器设计评估、体系结构研究和系统软件开发。在 gem5 提供的多种 CPU 模型中,O3 CPU(Out-of-Order CPU) 是最复杂、时序精度最高的模型,它模拟了一颗完整的乱序执行处理器的微架构。

O3 CPU 的设计** loosely based on Alpha 21264**,实现了经典的 Tomasulo 算法,支持寄存器重命名、乱序发射与执行、精确中断等现代高性能处理器的核心特性。与 gem5 中的 AtomicSimpleCPU 或 TimingSimpleCPU 等功能模型不同,O3 CPU 在每个周期内模拟流水线各阶段的活动,提供 cycle-level 的时序精度。

本文将深入剖析 gem5 O3 CPU 的微架构设计,涵盖流水线各阶段、核心数据结构、分支预测机制以及与内存子系统的交互,最后通过与 MinorCPU(in-order)的对比,揭示乱序执行引擎的设计精髓。


2. 整体架构概览

O3 CPU 采用 5 级流水线 + 1 个合并阶段 的结构:

1
2
3
4
5
Fetch ──→ Decode ──→ Rename ──→ IEW ──→ Commit

┌────────┼────────┐
│ │ │
Issue Execute Writeback

这五个阶段是:

阶段 功能
Fetch 从指令缓存取指,与分支预测器交互,创建 DynInst 对象
Decode 译码指令,将复杂指令拆分为微操作(micro-ops),提前解析无条件分支
Rename 寄存器重命名,从 FreeList 分配物理寄存器,更新 Scoreboard
IEW(Issue/Execute/Writeback) 从 Issue Queue 发射指令到功能单元,执行并写回结果,唤醒依赖
Commit 按程序顺序通过 ROB 提交指令,处理异常和中断

流水线交互机制:Time Buffer

各阶段之间通过 time buffer 通信,这是一种可配置延迟的 FIFO 通道。例如 fetchToDecodeDelay 参数控制从 Fetch 输出到 Decode 输入所需的周期数。每个阶段通过”向前 time buffer”向下游发送指令,通过”向后 time buffer”接收来自下游的 stall/squash 信号。

模板策略:ISA 无关设计

O3 CPU 使用 C++ 模板策略(template policy) 而非虚函数来实现多态。所有核心类(Fetch、Decode、DynInst、CPU)通过 Impl 类型参数作为模板参数传入:

1
2
3
4
5
template <class Impl>
class DefaultFetch { ... };

template <class Impl>
class DefaultCommit { ... };

这种设计的好处是:编译期即可确定所有类型信息,消除虚函数开销,同时保持 ISA 无关性。ISA 相关的代码被隔离在高层类(如 AlphaO3CPUX86O3CPU)中。


3. 流水线前端:Fetch / Decode / Rename

3.1 Fetch 阶段

Fetch 阶段负责从指令缓存中取指令,是 DynInst(Dynamic Instruction)对象首次创建 的地方。

核心职责:

  • 每周期取指(宽度可配置)
  • 在 SMT 模式下选择取指的线程(支持 ICOUNT、RoundRobin、Branch 等策略)
  • 与分支预测器交互,获取预测的下一 PC
  • 通过 IcachePort 接口与指令缓存通信

关键设计点:

Fetch 阶段持有 fetchBuffer 用于管理取回的缓存行。在取指过程中,分支预测器提供预测方向,Fetch 将预测的 nextPC 存储在 DynInst 中,后续流水线执行时会与实际结果比较。

Fetch 的输出通过 FetchStruct 传递给 Decode 阶段。

3.2 Decode 阶段

Decode 阶段对指令进行译码。值得注意的是,指令的 实际译码在 StaticInst 创建时(Fetch 阶段)就已经完成,因此 Decode 阶段的工作相对轻量:

  • 对指令进行合法性检查
  • 将复杂指令拆分为微操作(micro-ops)
  • 对 PC 相对的无条件分支进行早期解析

Decode 通过与 IEW/Rename/Commit 的向后 time buffer 接收 stall 和 squash 信号。

3.3 Rename 阶段

Rename 阶段是实现乱序执行的核心前置条件。它通过寄存器重命名消除 WAW(Write-After-Write)和 WAR(Write-After-Read) hazards。

核心组件:

  • RenameMap:维护体系结构寄存器 → 物理寄存器的映射关系。每个线程有两个映射表:
    • renameMap[tid]:当前 Rename 阶段使用的活跃映射
    • commitRenameMap[tid]:用于提交/恢复的快照映射
  • FreeList:管理空闲物理寄存器
  • Scoreboard:跟踪物理寄存器是否已就绪(值已计算出来)

重命名过程:

  1. 从 FreeList 分配新的物理寄存器作为目标寄存器
  2. 更新 RenameMap:arch reg → new phys reg
  3. 在 Scoreboard 中将新物理寄存器标记为”未就绪”
  4. 使用 RenameMap 查找源寄存器对应的物理寄存器
  5. 将重命名后的寄存器索引写入 DynInst

Stall 条件(Rename 暂停流水线的情况):

  • 空闲物理寄存器不足
  • 后端资源已满:IQ(指令队列)、ROB(重排序缓冲)、LQ(加载队列)、SQ(存储队列)
  • 遇到序列化指令(等待后端排空)

4. 流水线后端:Issue / Execute / Writeback

IEW 阶段是 O3 CPU 的核心,集成了发射、执行和写回三个子阶段。

4.1 整体流程

1
2
3
4
5
6
7
8
9
10
11
Rename::tick() → Rename::renameInsts()
↓ (插入 IQ + LSQ)
IEW::tick() → IEW::dispatchInsts()
↓ (调度到功能单元)
IEW::tick() → InstructionQueue::scheduleReadyInsts()
↓ (执行)
IEW::tick() → IEW::executeInsts()
↓ (唤醒依赖)
IEW::tick() → IEW::writebackInsts()
↓ (提交)
Commit::tick() → Commit::commitInsts()

4.2 Dispatch(分派)

指令经过 Rename 后进入 Dispatch 阶段,被插入到两个关键结构中:

  • Instruction Queue(IQ):所有指令(包括非访存指令)都进入 IQ
  • Load/Store Queue(LSQ):仅访存指令进入

如果 IQ 或 LSQ 已满,Dispatch 将 stall。

4.3 Issue(发射)

Instruction Queue(也称为 Issue Queue / 保留站)是乱序发射的核心。

工作机制:

  1. IQ 为每条指令跟踪其源寄存器的就绪状态
  2. 当指令的所有源寄存器均已就绪(通过 Scoreboard 判断),指令进入 ready list
  3. 每周期,scheduleReadyInsts() 从 ready list 中选择指令,分配到可用的功能单元(FU)
  4. 调度时指定功能单元的延迟(latency)

唤醒机制(Wakeup):

当一条指令执行完成并写回结果后,InstructionQueue::wakeDependents() 被调用。该方法扫描 IQ 中所有等待该结果的指令,将其标记为就绪,加入 ready list。

这里还有一个重要的优化:内存依赖预测(Memory Dependence Prediction)。IQ 中的 MemDepUnit 基于 store sets 算法预测访存指令之间的依赖关系,避免由于内存顺序不确定性而过度保守地阻塞发射。

4.4 Execute(执行)

O3 CPU 采用 execute-in-execute 模型——指令在 execute 阶段真正执行(而非在流水线入口处)。这与 SimpleScalar 等模拟器不同,能提供更高的时序精度和正确的乱序 load 交互建模。

执行阶段是实际发生以下操作的地方:

  • 运算指令:调用 execute() 计算结果,写入目标物理寄存器
  • Load 指令:通过 LSQ 发起内存访问
  • Store 指令:将数据写入 Store Queue(但不立即写缓存)
  • 分支指令:计算结果与预测比较,触发分支误预测修复

4.5 Writeback(写回)

写回阶段完成两件事:

  1. 更新 Scoreboard:将目标物理寄存器标记为”已就绪”
  2. 唤醒依赖指令:调用 wakeDependents(),通知 IQ 中等待该结果的指令

5. Commit 与精确异常

Commit 阶段保证指令按程序顺序退休,为处理器提供精确异常(Precise Interrupts)的能力。

5.1 提交流程

  • 每周期从 ROB 头部 retire 已就绪的指令
  • 退休时需要处理:
    • 正常提交:更新体系结构状态
    • 异常/陷阱:触发 squash
    • 中断:提交完成后处理外部中断

5.2 精确 Squash 机制

Commit 阶段为精确异常实现了一个两步骤 squash 机制

Step 1:当遇到导致异常的指令时,Commit 将该线程状态设置为 SquashAfterPending停止当前周期的提交。这样,最后提交的指令恰好是异常指令之前的那条。

Step 2下一个周期,Commit 检测到 SquashAfterPending 状态,立即 squash ROB 中所有未提交的指令(即异常指令及其之后的指令)。

这个机制确保异常指令本身不提交,而它之前的所有指令都已提交,从而实现精确异常。

5.3 Squash 信号广播

当 squash 发生时,Commit 向所有前端阶段(Fetch、Decode、Rename、IEW)广播:

  • 目标 PC(Fetch 应重定向到哪里)
  • Done 序列号(squash 到哪个序列号为止)
  • 是否为分支误预测(用于更新分支预测器)

Fetch 阶段通过 checkSignalsAndUpdate() 处理这些信号:

  • 如果误预测指令是控制指令 → 更新分支预测器
  • 否则(如内存顺序违规、异常)→ 仅清除预测器中的无效状态,不进行更新

5.4 多周期 Squash

O3 CPU 支持多周期 squash,模拟 ROB 每周期只能移除有限数量指令的现实硬件限制。


6. 核心数据结构

6.1 Reorder Buffer(ROB)

ROB 是乱序执行引擎的中枢,确保指令能按程序顺序提交。

结构:循环缓冲,由 headtail 迭代器管理 DynInstPtr 条目。

核心方法:

方法 功能
insertInst() 在尾部插入指令
readHeadInst() 读取最旧指令
retireHead() 提交并移除头部指令
squash(seqNum, tid) 将比 seqNum 新的所有指令标记为已 squash
isFull() / isEmpty() 容量检查

SMT 分区策略:

  • Dynamic:线程自由竞争 ROB 条目
  • Partitioned:每个线程固定分配
  • Threshold:每个线程有上限,但可共享剩余条目

6.2 Instruction Queue(IQ)

IQ 是乱序发射的核心,相当于保留站(Reservation Station)。

功能

  • 跟踪指令间的数据依赖
  • 维护 ready list(所有源操作数已就绪的指令)
  • 每周期选择就绪指令发射到功能单元
  • 实现 wakeup(结果写回后唤醒依赖指令)

内存依赖预测: IQ 中的 MemDepUnit 使用 store sets 算法预测访存指令间的顺序依赖,避免不必要的阻塞。

6.3 Load/Store Queue(LSQ)

LSQ 管理所有访存操作,包含 per-thread 的 Load Queue(LQ)和 Store Queue(SQ)。

Load 指令流程:

1
2
3
4
5
6
7
8
ExecuteLoad
→ LSQUnit::read() // 检查 store-to-load 转发
→ 可转发 → 直接从 SQ 取数据
→ 部分重叠 → stall,等待 store 写回
→ 无依赖 → 发送到 data cache

Cache 响应返回
→ LSQUnit::writeback() // 将加载值写入目标寄存器

Store 指令流程:

1
2
3
4
5
ExecuteStore
→ LSQUnit::write() // 仅拷贝数据到 SQ,不发送到缓存

Commit 后
→ LSQUnit::writebackStores() // 将已提交的 store 发送到 data cache

Store-to-Load Forwarding: LSQ 在执行 load 时检查是否与之前的 store 地址别名。如果完全重叠,直接从 store queue 转发数据;如果部分重叠,则 stall load 直到 store 完成写回。

内存顺序违规检测: checkViolation() 扫描 load queue,如果发现内存顺序违规,触发 IEW::squashDueToMemOrder() 进行修复。

6.4 物理寄存器文件(Physical Register File)

与体系结构寄存器文件不同,物理寄存器文件提供更多的寄存器,用于支持重命名。寄存器通过 FreeList 管理分配,通过 Scoreboard 跟踪就绪状态。


7. 分支预测机制

分支预测是高性能处理器的关键组成部分。O3 CPU 支持多种分支预测器,通过 --bp-type 参数或在配置脚本中指定 system.cpu.branchPred 来切换。

7.1 支持的预测器类型

预测器 类型 说明
LocalBP 局部预测 基于指令地址的 2-bit 饱和计数器
BiModeBP 双模预测 方向预测器 + 选择预测器
TournamentBP 竞赛预测 组合局部 + 全局预测
LTAGE TAGE 推荐:几何历史长度标签预测
MultiperspectivePerceptron 神经网络 感知机预测器(但存在推测执行 bug)
TAGE-SC-L TAGE+统计校正 最先进,但 gem5 实现存在问题

7.2 TournamentBP(竞赛预测器)

竞赛预测器组合了两种预测机制:

  • 局部预测器:每个分支有独立的历史表
  • 全局预测器:使用全局历史寄存器(GHRegister)记录最近分支的结果
  • 选择器:2-bit 饱和计数器,选择当前哪个预测器更准确

选择器需要两次误预测才切换偏好,确保不会因噪声而频繁切换。

7.3 L-TAGE 预测器

L-TAGE 是当前 gem5 中推荐的高性能分支预测器,基于 André Seznec 的 TAGE 算法(多次 CBP 冠军)。

核心思想: 使用多个标签表(tagged tables),每个表由不同长度的历史索引构成,历史长度按几何级数增长(如 4, 8, 16, 32, 64, 128, 256 bits)。

预测过程:

  1. 每个 TAGE 表用 PC 和对应历史长度的 hash 索引
  2. 查找匹配标签且历史长度最长的表
  3. 如果该表有命中,使用其预测
  4. 如果无命中,回退到基础预测器(如 bimodal)

有用计数器(Useful Counter): 每个条目跟踪其预测准确性,用于分配决策。当新条目需要分配时,有用计数器最低的条目被优先替换。

需要注意: gem5 中 TAGE-SC-L 的统计校正器(Statistical Corrector)在推测执行上下文中存在未完整实现的 bug——它基于提交状态而非推测状态的 history 进行训练,导致准确性低于理论预期。因此在 gem5 中使用 L-TAGE 作为高性能预测器是更稳妥的选择。


8. Cache 层次交互

O3 CPU 与 gem5 的 Cache 子系统通过经典的 requests 和 response 端口 交互。

8.1 指令缓存交互

Fetch 阶段通过 IcachePort 发送取指请求到指令缓存。如果缓存未命中,Fetch 阶段将 stall,等待缓存行填充完成。

8.2 数据缓存交互

数据缓存的交互更为复杂,主要原因是 LSQ 的存在:

Load 访问路径:

1
2
3
4
5
6
LSQUnit::read()
→ LSQRequest::buildPackets() // 构造数据包
→ LSQRequest::sendPacketToCache() // 发送到 data cache
→ DcachePort::recvTimingResp() // 接收响应
→ LSQUnit::completeDataAccess() // 完成数据访问
→ LSQUnit::writeback() // 写回结果到寄存器

Store 访问路径(写缓存发生在提交之后):

1
2
3
Commit 完成 → LSQUnit::writebackStores()
→ LSQRequest::buildPackets()
→ LSQRequest::sendPacketToCache()

Store 的延迟写回设计符合现代处理器的常见做法:store 在提交之前仅将数据写入 store queue,不修改缓存状态,从而保证在异常或误预测时不会污染缓存。

8.3 Cache 带宽控制

LSQ 使用 cacheStorePortscacheLoadPorts 参数控制每周期访问缓存的端口数量,模拟真实硬件中有限的缓存端口资源。


9. 与 In-Order CPU 的对比

gem5 提供了两种主要的 inorder CPU 模型:TimingSimpleCPU 和 MinorCPU。我们以 MinorCPU 为例与 O3 CPU 对比。

9.1 MinorCPU 简介

MinorCPU 是 gem5 的 in-order 流水线 CPU 模型,基于 ARM Cortex-A53 的设计思路,具有四级流水线:

1
Fetch1 → Fetch2 → Decode → Execute

它使用 scoreboard 来跟踪功能单元和寄存器的忙闲状态,但指令仍然按程序顺序发射。当发生 RAW 冒险时,流水线停顿而非乱序执行。

9.2 关键区别

维度 O3 CPU MinorCPU
执行模型 乱序执行(out-of-order) 顺序执行(in-order)
寄存器重命名 ✅ 支持(FreeList + RenameMap) ❌ 不支持
ROB ✅ Reorder Buffer ❌ 无
Issue Queue ✅ 多条目发射队列 ❌ 无(或极简)
LSQ ✅ 完整的 load/store queue ✅ 有但较简单
分支预测 高级(L-TAGE/Tournament) 较简单
模拟速度 (~2× 慢于 MinorCPU)
时序精度 最高 较高
适用场景 高性能 OoO 核研究 嵌入式/in-order 核研究

9.3 模拟性能数据

根据 MinorCPU 原提交者的基准测试数据(ARM 平台):

基准测试 O3 CPU MinorCPU
10.linux-boot 1883s 1075s
10.mcf 967s 491s
20.parser 6315s 3146s
30.eon 3413s 2414s

MinorCPU 大约比 O3 CPU 快 2 倍,这是合理的,因为乱序执行需要模拟更多的硬件结构和调度逻辑。

9.4 何时使用哪个模型

  • 研究乱序执行、寄存器重命名、ROB 管理等 OoO 微架构 → O3 CPU
  • 评估 IPC 性能、瓶颈分析 → O3 CPU
  • 嵌入式处理器、简单 inorder 核建模 → MinorCPU
  • 快速启动 Linux、功能验证 → AtomicSimpleCPU 或 TimingSimpleCPU

10. 关键设计决策与总结

设计决策回顾

1. Execute-in-Execute 模型

O3 CPU 在 Execute 阶段真正模拟指令执行,而非在流水线入口处。这一设计选择提供了更精确的时序建模,尤其是在乱序 load 交互方面,能够正确模拟 load 指令在乱序环境中访问缓存的时序行为。

2. 模板策略而非虚函数

通过 C++ 模板实现 ISA 无关的流水线阶段代码,在保持代码复用的同时消除虚函数开销。这意味着为新的 ISA 移植 O3 CPU 时,只需要实现 ISA 相关的部分(指令执行逻辑、译码等)。

3. 非推测性的 Store 写缓存

Store 数据在提交之前仅写入 store queue,不发送到缓存。这确保了在异常或误预测导致 squash 时,缓存状态不会被污染,是实现精确异常的重要一环。

4. 两步骤 Squash 机制

Commit 阶段的 SquashAfterPending 机制确保了精确异常的语义:异常指令本身不提交,其之前的所有指令完整提交。

5. Time Buffer 实现阶段间通信

使用可配置延迟的 time buffer 而非简单的流水线寄存器,使得 O3 CPU 能够模拟不同流水线深度的行为,增加了模型的灵活性。

总结

gem5 O3 CPU 是一个全面的乱序执行处理器模型,涵盖了现代高性能处理器的所有关键微架构特性:

  • 前端采用 Fetch-Decode-Rename 的经典流水线,支持高级分支预测
  • 后端通过 Instruction Queue、物理寄存器文件和 Load/Store Queue 实现乱序发射与执行
  • 提交阶段通过 ROB 保证精确异常,支持 SMT 多线程
  • 内存交互通过 LSQ 管理所有访存操作,支持 store-to-load forwarding 和内存顺序违规检测

O3 CPU 的设计借鉴了 Alpha 21264 等真实处理器的微架构思路,同时通过参数化配置(流水线宽度、队列深度、功能单元数量等)保持了极大的灵活性。对于体系结构研究者和学生而言,深入理解 O3 CPU 的微架构不仅有助于掌握 gem5 的使用,更能加深对乱序执行处理器设计原理的理解。

作为 gem5 生态中最复杂的 CPU 模型,O3 CPU 在模拟速度和精度之间取得了良好的平衡,是进行乱序处理器微架构研究和性能分析的理想平台。


本文基于 gem5 官方文档、源码(O3CPU 模型)、以及相关学术论文编写,参考版本为 gem5 24.0+。