GEM5 O3 CPU Rename Stage 源码解析
1. 引言
Rename (重命名) 阶段是乱序处理器 (Out-of-Order Processor) 的核心,其主要职责是消除写后读 (WAR) 和写后写 (WAW) 伪相关,将指令中的体系结构寄存器 (Architectural Registers) 映射到一组数量更多的物理寄存器 (Physical Registers) 上,从而发掘指令级并行性。
本文档将深入分析 GEM5 O3 CPU Rename 阶段的实现,重点关注其关键数据结构和核心逻辑,帮助你理解其工作原理。
2. 关键数据结构
Rename 阶段的实现主要围绕 Rename 类 (src/cpu/o3/rename.hh, src/cpu/o3/rename.cc) 和 UnifiedRenameMap 类 (src/cpu/o3/rename_map.hh) 展开。
2.1. Rename 类
Rename 类是重命名阶段的顶层封装,负责管理整个阶段的状态、数据流和逻辑。
-
renameStatus[MaxThreads]:enum ThreadStatus这是一个为每个线程维护的状态机,用于描述 Rename 阶段当前所处的状态,如Running(正常运行)、Idle(空闲)、Blocked(阻塞)、Squashing(正在清空)、Unblocking(正在解除阻塞) 等。tick()函数的行为很大程度上取决于这个状态。 -
insts[MaxThreads]:InstQueue(即std::deque<DynInstPtr>) 用于暂存当前周期从Decode阶段传递过来的指令。 -
skidBuffer[MaxThreads]:InstQueue这是 Rename 阶段和 Decode 阶段之间的"缓冲垫"。当 Rename 阶段因为某些原因 (如后端资源不足)Blocked时,从 Decode 阶段新来的指令就会被存入skidBuffer。当unblock时,Rename 会优先处理skidBuffer中的指令。这是一种处理流水线反压 (backpressure) 的经典机制。 -
historyBuffer[MaxThreads]:std::list<RenameHistory>这是实现推测执行和精确异常的关键数据结构。每当一个指令的目的寄存器被重命名,一条记录 (RenameHistory) 就会被添加到historyBuffer的头部。 RenameHistory结构体包含:instSeqNum: 指令的序列号。archReg: 体系结构寄存器。newPhysReg: 新映射的物理寄存器。prevPhysReg: 该体系结构寄存器之前映射的物理寄存器。
-
historyBuffer的作用:- 指令Squash: 当发生分支预测错误或异常时,流水线需要被清空 (squash)。Rename 阶段会从新到旧遍历
historyBuffer,将所有被 squash 的指令的重命名操作"撤销",即将体系结构寄存器的映射恢复到prevPhysReg,并释放newPhysReg。 - 指令Commit: 当一条指令成功提交 (commit) 后,其
prevPhysReg就再也不会被任何非推测性指令所需要了。此时,historyBuffer中对应的记录会被移除,并且prevPhysReg会被真正地释放回freeList中,以供后续指令使用。
- 指令Squash: 当发生分支预测错误或异常时,流水线需要被清空 (squash)。Rename 阶段会从新到旧遍历
-
renameMap[MaxThreads]:UnifiedRenameMap *指向UnifiedRenameMap的指针,负责维护体系结构寄存器到物理寄存器的映射关系。详见 2.2节。 -
freeList:UnifiedFreeList *指向UnifiedFreeList的指针,负责管理所有可用的物理寄存器。当需要为目的寄存器分配新的物理寄存器时,就会从freeList中获取。 -
scoreboard:Scoreboard *记分板,用于跟踪每个物理寄存器是否已经准备好 (即计算出结果)。在重命名源寄存器时,Rename 阶段会查询scoreboard来确定源操作数是否就绪。 -
流水线通信接口:
fromDecode: 从 Decode 阶段接收指令。toIEW: 将重命名后的指令发送到 IEW (Issue/Execute/Writeback) 阶段。fromIEW,fromCommit: 从后续阶段接收反馈信号,如 ROB/IQ/LSQ 的空闲条目数、squash 信号等。
2.2. UnifiedRenameMap 和 SimpleRenameMap
UnifiedRenameMap 是一个统一的重命名映射表,它内部为每种寄存器类型 (如整数、浮点、向量等) 都包含一个 SimpleRenameMap。
SimpleRenameMap:map: 一个std::vector<VirtRegId>,是实际的映射表,map[arch_reg_idx]存储了该体系结构寄存器当前映射到的物理寄存器 ID (VirtRegId)。freeList: 指向对应类型寄存器的SimpleFreeList。-
rename(arch_reg, bypass_reg): 核心方法。它从freeList获取一个新的物理寄存器,更新map,并返回新的和旧的物理寄存器。bypass_reg用于实现移动消除(move elimination)优化。 -
UnifiedRenameMap: rename(dest_reg, bypass_reg): 根据dest_reg的类型,将调用分发给对应的SimpleRenameMap。lookup(arch_reg): 查找一个体系结构寄存器当前映射到的物理寄存器。setEntry(arch_reg, virt_reg): 直接设置一个映射关系,主要用于 squash 时恢复旧的映射。
3. 核心代码逻辑
Rename 阶段的逻辑在每个时钟周期的 tick() 函数中被驱动。
3.1. tick() - 周期性的主循环
tick() 函数是 Rename 阶段的入口,其主要流程如下:
-
sortInsts(): 从fromDecode接口读取指令,并根据线程ID分发到各自的insts队列中。 -
遍历Active Threads: 对每个活跃的线程执行以下操作。
-
checkSignalsAndUpdate(tid):- 检查来自 Commit 阶段的
squash信号。如果存在,则调用squash()。 - 检查来自 IEW 等后续阶段的
stall信号。 - 根据这些信号和当前的资源状况 (如 ROB 是否满),更新当前线程的
renameStatus。例如,如果之前是Blocked状态且阻塞条件已解除,则切换到Unblocking状态。
- 检查来自 Commit 阶段的
-
rename(status_change, tid):- 这是一个分发函数,根据
renameStatus调用核心的renameInsts()。 - 如果状态是
Running或Idle, 对insts队列中的新指令进行重命名。 - 如果状态是
Unblocking, 对skidBuffer中缓存的指令进行重命名。 - 如果状态是
Blocked或Squashing, 则不进行重命名,只更新统计信息。
- 这是一个分发函数,根据
3.2. renameInsts(tid) - 指令重命名核心
这是实际执行重命名工作的主函数,其逻辑非常关键:
-
选择指令源: 根据
renameStatus判断是从insts队列还是skidBuffer获取指令。 -
资源检查:
- 调用
calcFreeROBEntries(),calcFreeIQEntries(),calcFreeLQEntries(),calcFreeSQEntries()计算 ROB、IQ (Issue Queue)、LQ/SQ (Load/Store Queue) 的可用空间。这些计算会考虑已发送但尚未被后续阶段确认的 "in-flight" 指令。 - 如果任何一个关键资源耗尽,则调用
block(tid)将当前阶段设为阻塞状态,并将未处理的指令存入skidBuffer,然后返回。
- 调用
-
物理寄存器预检查 (
canRename()): 在开始重命名之前,会检查freeList中是否有足够数量、正确类型的物理寄存器来满足本周期内待重命名指令的需求。这是一个重要的预判,避免在重命名中途因缺少物理寄存器而卡住。 -
循环重命名: 在一个
while循环中,逐条处理指令,直到达到renameWidth(重命名带宽) 或没有更多指令/资源。-
renameSrcRegs(inst, tid):- 遍历指令的所有源寄存器。
- 对每个源寄存器,调用
renameMap->lookup()找到其映射的物理寄存器。 - 查询
scoreboard检查该物理寄存器的数据是否就绪,并相应地标记指令的源操作数状态。 - 如果每一个src 源寄存器都ready,则调用
setCanIssue()标记指令状态为CanIssue。
-
renameDestRegs(inst, tid):- 遍历指令的所有目的寄存器。
- 调用
renameMap->rename():renameMap从freeList获取一个新物理寄存器 (newPhysReg)。- 更新映射表,使
archReg指向newPhysReg。 - 返回
newPhysReg和archReg之前映射的prevPhysReg。
- 创建历史记录: 将
instSeqNum,archReg,newPhysReg,prevPhysReg打包成一个RenameHistory对象,并压入historyBuffer的头部。 - 在
scoreboard中将newPhysReg标记为 "未就绪"。 - 优化: 此处实现了移动消除 (Move Elimination) 和常量折叠 (Constant Folding)。例如,对于
mov x1, x2指令,可以直接让x1的映射指向x2当前映射的物理寄存器,从而消除一条指令。
- 发送到IEW: 将重命名完成的指令放入
toIEW队列,发送给下一阶段。
-
3.3. squash() 和 removeFromHistory() - 推测执行的支柱
这两个函数与 historyBuffer 紧密配合,是实现精确状态恢复的核心。
doSquash(squash_seq_num, tid):- 当收到 squash 信号时被调用。
- 从
historyBuffer的头部 (最新条目) 开始遍历。 -
对于所有序列号大于
squash_seq_num的历史记录:- 调用
renameMap->setEntry()将体系结构寄存器的映射恢复到prevPhysReg。 - 调用
tryFreePReg(newPhysReg)释放为错误路径指令分配的物理寄存器。tryFreePReg会减少物理寄存器的引用计数,当计数为0时才将其归还freeList。 - 从
historyBuffer中删除该条目。
- 调用
-
removeFromHistory(inst_seq_num, tid): - 当 Commit 阶段通知有指令成功提交时被调用。
- 从
historyBuffer的尾部 (最老条目) 开始遍历。 - 对于所有序列号小于等于
inst_seq_num的历史记录:- 这条记录中的
prevPhysReg现在可以被安全地回收了,因为它不再被任何有效的体系结构状态引用。 - 调用
tryFreePReg(prevPhysReg)释放这个旧的物理寄存器。 - 从
historyBuffer中删除该条目。
- 这条记录中的
4. 总结
GEM5 的 Rename 阶段是一个精心设计的流水级,它通过一系列关键数据结构和清晰的逻辑实现了现代乱序处理器的核心功能:
UnifiedRenameMap和UnifiedFreeList构成了寄存器重命名的基础。historyBuffer是实现推测执行、精确中断和高效物理寄存器回收的基石。skidBuffer提供了灵活的流水线流控机制。tick()函数中的状态机 (renameStatus) 清晰地组织了不同场景下的行为,如正常执行、阻塞和清空。
通过理解这些组件如何协同工作,可以深入掌握乱序处理器中消除数据伪相关、支持推测执行的精髓。
5. 辅助数据结构
Rename 阶段的顺利运行依赖于几个关键的辅助数据结构,它们共同构成了物理寄存器管理和数据依赖跟踪的完整体系。
5.1. 物理寄存器堆 (PhysRegFile)
PhysRegFile (src/cpu/o3/regfile.hh) 是物理寄存器的“实体”所在。它并不直接参与重命名逻辑,而是作为物理寄存器的中央存储和管理器。
- 核心职责:
- 物理存储: 内部为每种寄存器类型 (Int, Float, Vector等) 维护一个独立的
RegFile对象,这些对象是真正存储寄存器值的地方。 - ID 工厂: 在初始化阶段,
PhysRegFile会创建所有物理寄存器的唯一标识符 (PhysRegId)。每个PhysRegId包含了寄存器类型、类型内的索引,以及一个全局唯一的扁平索引 (flatIndex)。 - 初始化 FreeList:
PhysRegFile在创建完所有的PhysRegId后,会将它们全部添加到UnifiedFreeList中,为 Rename 阶段提供可用的物理寄存器池。 - 数据访问: 在流水线的后端 (如 Writeback 阶段),当指令计算完成需要写入结果时,会通过
PhysRegId在PhysRegFile中找到对应的位置并调用setReg()写入数据。同样,当需要读取寄存器值时,也会通过getReg()进行。
可以把 PhysRegFile 理解为银行金库,它保管着所有的黄金(数据); PhysRegId 是金块的编号; FreeList 是记录哪些金块可用的账本; 而 RenameMap 则是记录哪个客户(体系结构寄存器)当前拥有哪个金块(物理寄存器)的账本。
5.2. 记分板 (Scoreboard)
Scoreboard (src/cpu/o3/scoreboard.hh) 是一个非常简单但至关重要的数据依赖跟踪机制。
- 实现: 其核心是一个
std::vector<bool>。这个向量的大小等于物理寄存器的总数,使用PhysRegId的flatIndex()作为索引。 - 工作流程:
- 分配时 (Rename): 当
Rename阶段为一个指令的目的寄存器分配了一个新的物理寄存器时,它会调用scoreboard->unsetReg()将对应物理寄存器的状态位设置为false(未就绪)。 - 检查时 (Rename): 当
Rename阶段处理一个指令的源寄存器时,它会调用scoreboard->getReg()来检查其映射的物理寄存器状态位。如果为false,则该源操作数未就绪,指令在进入 Issue Queue 后需要等待。 - 写回时 (Writeback): 当一条指令的计算结果被写回到
PhysRegFile时,Writeback阶段会调用scoreboard->setReg()将对应物理寄存器的状态位设置为true(已就绪)。这会通知所有等待该寄存器的指令,它们的操作数已经准备好了。
Scoreboard 完美地解决了乱序执行中的读后写 (RAW) 数据相关问题,确保指令只有在所有源操作数都可用时才会被执行。
5.3. 虚拟寄存器ID (VirtRegId) 与优化
VirtRegId (src/cpu/o3/regfile.hh) 是对 PhysRegId 的一层封装,它是实现移动消除和常量折叠等高级优化的关键。
- 结构:
VirtRegId包含两个主要部分: phyReg: 一个指向物理寄存器ID (PhysRegIdPtr) 的指针。-
ieop: 一个指向IEOperand的智能指针。IEOperand(Immediate Early Operand) 代表一个可以被“吸收”或“折叠”的立即数操作。 -
优化原理:
- 移动消除 (Move Elimination): 当
Rename阶段遇到一条mov rd, rs指令时,它不会分配一个新的物理寄存器。取而代之的是,它在RenameMap中直接让rd指向rs当前映射的VirtRegId。这样,后续使用rd的指令实际上会直接使用rs的物理寄存器,mov指令本身则变成了一个空操作 (Nop),无需进入执行阶段。 - 常量折叠 (Constant Folding): 当遇到
addi rd, rs, imm1这样的指令时,Rename阶段会为rd分配一个新的物理寄存器,并创建一个VirtRegId。这个VirtRegId不仅包含新的phyReg,还包含一个IEOperand,记录了type=ADD和imm=imm1。如果紧接着有一条指令addi r_new, rd, imm2,Rename阶段可以检查到rd的VirtRegId中有一个待处理的ADD操作,于是它可以将两个立即数相加,为r_new创建一个新的VirtRegId,其IEOperand记录为imm=imm1+imm2。这样就将两条addi指令折叠成了一条。
通过 VirtRegId 这种方式,Rename 阶段超越了简单的寄存器映射,具备了在流水线前端进行微架构级别优化的能力,从而减少了后端执行单元的压力,提升了处理器的整体性能。
6. Scoreboard 在现代架构中的角色
您提出的关于 Scoreboard 的问题非常关键,它触及了现代乱序处理器设计的核心。GEM5 O3 CPU 的架构是现代的 寄存器重命名/物理寄存器堆 (Register Renaming/PRF) 架构,是 Tomasulo 算法的演进。而 Scoreboard 在其中扮演的是一个高度特化、角色单一的辅助角色,它不是传统意义上那个负责全局调度、会产生 WAW 冒险阻塞的记分牌。
6.1. GEM5 O3 CPU 架构分解
GEM5 将现代乱序处理器的功能拆分到了不同的模块中,这是一种更模块化、更接近硬件设计的思路:
-
Rename阶段 +UnifiedRenameMap+UnifiedFreeList:- 这三个组件协同工作,完美对应了现代模型中的
rename_table和free_list。 - 它们的核心职责是消除命名冒险 (WAW/WAR)。通过为每条指令的目的寄存器从
free_list分配一个新的物理寄存器,并更新rename_map,从根本上解决了这些伪相关,而不是像传统记分牌那样去检测并阻塞它们。
- 这三个组件协同工作,完美对应了现代模型中的
-
PhysRegFile(物理寄存器堆):- 这对应了现代模型中物理寄存器里的
value部分。 - 它是一个纯粹的数据存储阵列,是所有物理寄存器真实值的存放地。
- 这对应了现代模型中物理寄存器里的
-
Scoreboard(记分板):- 这精确地对应了现代模型中物理寄存器里的
ready位。 - GEM5 的
Scoreboard的唯一职责就是: 跟踪每一个物理寄存器的“就绪”状态。 - 它的内部实现极其简单,就是一个
std::vector<bool>,用物理寄存器的全局唯一ID (flatIndex) 作为索引。 - 它不包含任何复杂的调度逻辑。它只回答一个问题:“这个物理寄存器的数据准备好了吗?”
- 这精确地对应了现代模型中物理寄存器里的
6.2. 工作流程串讲
让我们把这些组件在一条指令的生命周期中串联起来:
-
Rename 阶段:
- 一条指令
add r3, r1, r2进入 Rename 阶段。 - 源寄存器: 查找
rename_map得到r1和r2当前映射的物理寄存器p10和p12。然后,它会去问Scoreboard:“p10ready 吗?”、“p12ready 吗?”。这个信息会随着指令一起传递下去。 - 目的寄存器: 为
r3从free_list申请一个新的物理寄存器,比如p25。然后更新rename_map,让r3 -> p25。同时,它会立刻通知Scoreboard:“p25现在不 ready 了 (unsetReg(p25))”,因为它的值即将被这条add指令计算。
- 一条指令
-
Issue Queue (发射队列):
- 重命名后的指令被放入发射队列。发射队列的逻辑会持续监控
Scoreboard。 - 只有当指令的所有源物理寄存器 (
p10,p12) 在Scoreboard中的状态都变为ready时,这条指令才可能被发射到执行单元。
- 重命名后的指令被放入发射队列。发射队列的逻辑会持续监控
-
Writeback (写回) 阶段:
add指令执行完毕,计算出了结果。- 结果被写入
PhysRegFile中p25寄存器所在的位置。 - 同时,写回阶段会立即通知
Scoreboard:“p25现在已经 ready 了 (setReg(p25))”。
-
唤醒:
Scoreboard中p25的状态变为ready后,发射队列中所有等待p25的指令都会被“唤醒”,它们的一个依赖条件满足了。
6.3. 结论
所以,GEM5 的实现确实比传统 Scoreboard 更先进。它的架构是现代的,而 Scoreboard 只是这个现代架构中用于实现“ready 位跟踪”功能的一个具体组件。
| 现代处理器模型组件 | GEM5 O3 CPU 对应组件 |
|---|---|
rename_table |
UnifiedRenameMap |
free_list |
UnifiedFreeList |
PhysicalRegister.value |
PhysRegFile |
PhysicalRegister.ready |
Scoreboard |
PhysicalRegister.producer_rob |
这个追踪逻辑分布在 ROB 和指令对象 DynInst 自身中 |
GEM5 之所以保留 Scoreboard 这个名字,很可能是出于历史和习惯,但它的功能已经从一个中央集权的“调度官”退化成了一个分布式的“状态查询服务”。真正的“智能”存在于 Rename 阶段的重命名逻辑和 Issue Queue 的指令唤醒逻辑中。