跳转至

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 的作用:

    1. 指令Squash: 当发生分支预测错误或异常时,流水线需要被清空 (squash)。Rename 阶段会从新到旧遍历 historyBuffer,将所有被 squash 的指令的重命名操作"撤销",即将体系结构寄存器的映射恢复到 prevPhysReg,并释放 newPhysReg
    2. 指令Commit: 当一条指令成功提交 (commit) 后,其 prevPhysReg 就再也不会被任何非推测性指令所需要了。此时,historyBuffer 中对应的记录会被移除,并且 prevPhysReg 会被真正地释放回 freeList 中,以供后续指令使用。
  • 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. UnifiedRenameMapSimpleRenameMap

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 阶段的入口,其主要流程如下:

  1. sortInsts(): 从 fromDecode 接口读取指令,并根据线程ID分发到各自的 insts 队列中。

  2. 遍历Active Threads: 对每个活跃的线程执行以下操作。

  3. checkSignalsAndUpdate(tid):

    • 检查来自 Commit 阶段的 squash 信号。如果存在,则调用 squash()
    • 检查来自 IEW 等后续阶段的 stall 信号。
    • 根据这些信号和当前的资源状况 (如 ROB 是否满),更新当前线程的 renameStatus。例如,如果之前是 Blocked 状态且阻塞条件已解除,则切换到 Unblocking 状态。
  4. rename(status_change, tid):

    • 这是一个分发函数,根据 renameStatus 调用核心的 renameInsts()
    • 如果状态是 RunningIdle, 对 insts 队列中的新指令进行重命名。
    • 如果状态是 Unblocking, 对 skidBuffer 中缓存的指令进行重命名。
    • 如果状态是 BlockedSquashing, 则不进行重命名,只更新统计信息。

3.2. renameInsts(tid) - 指令重命名核心

这是实际执行重命名工作的主函数,其逻辑非常关键:

  1. 选择指令源: 根据 renameStatus 判断是从 insts 队列还是 skidBuffer 获取指令。

  2. 资源检查:

    • 调用 calcFreeROBEntries(), calcFreeIQEntries(), calcFreeLQEntries(), calcFreeSQEntries() 计算 ROB、IQ (Issue Queue)、LQ/SQ (Load/Store Queue) 的可用空间。这些计算会考虑已发送但尚未被后续阶段确认的 "in-flight" 指令。
    • 如果任何一个关键资源耗尽,则调用 block(tid) 将当前阶段设为阻塞状态,并将未处理的指令存入 skidBuffer,然后返回。
  3. 物理寄存器预检查 (canRename()): 在开始重命名之前,会检查 freeList 中是否有足够数量、正确类型的物理寄存器来满足本周期内待重命名指令的需求。这是一个重要的预判,避免在重命名中途因缺少物理寄存器而卡住。

  4. 循环重命名: 在一个 while 循环中,逐条处理指令,直到达到 renameWidth (重命名带宽) 或没有更多指令/资源。

    • renameSrcRegs(inst, tid):

      • 遍历指令的所有源寄存器。
      • 对每个源寄存器,调用 renameMap->lookup() 找到其映射的物理寄存器。
      • 查询 scoreboard 检查该物理寄存器的数据是否就绪,并相应地标记指令的源操作数状态。
      • 如果每一个src 源寄存器都ready,则调用 setCanIssue() 标记指令状态为 CanIssue
    • renameDestRegs(inst, tid):

      • 遍历指令的所有目的寄存器。
      • 调用 renameMap->rename():
        • renameMapfreeList 获取一个新物理寄存器 (newPhysReg)。
        • 更新映射表,使 archReg 指向 newPhysReg
        • 返回 newPhysRegarchReg 之前映射的 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 的历史记录:

    1. 调用 renameMap->setEntry() 将体系结构寄存器的映射恢复到 prevPhysReg
    2. 调用 tryFreePReg(newPhysReg) 释放为错误路径指令分配的物理寄存器。tryFreePReg 会减少物理寄存器的引用计数,当计数为0时才将其归还 freeList
    3. historyBuffer 中删除该条目。
  • removeFromHistory(inst_seq_num, tid):

  • 当 Commit 阶段通知有指令成功提交时被调用。
  • historyBuffer 的尾部 (最老条目) 开始遍历。
  • 对于所有序列号小于等于 inst_seq_num 的历史记录:
    1. 这条记录中的 prevPhysReg 现在可以被安全地回收了,因为它不再被任何有效的体系结构状态引用。
    2. 调用 tryFreePReg(prevPhysReg) 释放这个旧的物理寄存器。
    3. historyBuffer 中删除该条目。

4. 总结

GEM5 的 Rename 阶段是一个精心设计的流水级,它通过一系列关键数据结构和清晰的逻辑实现了现代乱序处理器的核心功能:

  • UnifiedRenameMapUnifiedFreeList 构成了寄存器重命名的基础。
  • 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 阶段),当指令计算完成需要写入结果时,会通过 PhysRegIdPhysRegFile 中找到对应的位置并调用 setReg() 写入数据。同样,当需要读取寄存器值时,也会通过 getReg() 进行。

可以把 PhysRegFile 理解为银行金库,它保管着所有的黄金(数据); PhysRegId 是金块的编号; FreeList 是记录哪些金块可用的账本; 而 RenameMap 则是记录哪个客户(体系结构寄存器)当前拥有哪个金块(物理寄存器)的账本。

5.2. 记分板 (Scoreboard)

Scoreboard (src/cpu/o3/scoreboard.hh) 是一个非常简单但至关重要的数据依赖跟踪机制。

  • 实现: 其核心是一个 std::vector<bool>。这个向量的大小等于物理寄存器的总数,使用 PhysRegIdflatIndex() 作为索引。
  • 工作流程:
  • 分配时 (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=ADDimm=imm1。如果紧接着有一条指令 addi r_new, rd, imm2, Rename 阶段可以检查到 rdVirtRegId 中有一个待处理的 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 将现代乱序处理器的功能拆分到了不同的模块中,这是一种更模块化、更接近硬件设计的思路:

  1. Rename 阶段 + UnifiedRenameMap + UnifiedFreeList:

    • 这三个组件协同工作,完美对应了现代模型中的 rename_tablefree_list
    • 它们的核心职责是消除命名冒险 (WAW/WAR)。通过为每条指令的目的寄存器从 free_list 分配一个新的物理寄存器,并更新 rename_map,从根本上解决了这些伪相关,而不是像传统记分牌那样去检测并阻塞它们。
  2. PhysRegFile (物理寄存器堆):

    • 这对应了现代模型中物理寄存器里的 value 部分。
    • 它是一个纯粹的数据存储阵列,是所有物理寄存器真实值的存放地。
  3. Scoreboard (记分板):

    • 这精确地对应了现代模型中物理寄存器里的 ready 位。
    • GEM5 的 Scoreboard 的唯一职责就是: 跟踪每一个物理寄存器的“就绪”状态。
    • 它的内部实现极其简单,就是一个 std::vector<bool>,用物理寄存器的全局唯一ID (flatIndex) 作为索引。
    • 包含任何复杂的调度逻辑。它只回答一个问题:“这个物理寄存器的数据准备好了吗?”

6.2. 工作流程串讲

让我们把这些组件在一条指令的生命周期中串联起来:

  1. Rename 阶段:

    • 一条指令 add r3, r1, r2 进入 Rename 阶段。
    • 源寄存器: 查找 rename_map 得到 r1r2 当前映射的物理寄存器 p10p12。然后,它会去问 Scoreboard:“p10 ready 吗?”、“p12 ready 吗?”。这个信息会随着指令一起传递下去。
    • 目的寄存器: 为 r3free_list 申请一个新的物理寄存器,比如 p25。然后更新 rename_map,让 r3 -> p25。同时,它会立刻通知 Scoreboard:“p25 现在 ready 了 (unsetReg(p25))”,因为它的值即将被这条 add 指令计算。
  2. Issue Queue (发射队列):

    • 重命名后的指令被放入发射队列。发射队列的逻辑会持续监控 Scoreboard
    • 只有当指令的所有源物理寄存器 (p10, p12) 在 Scoreboard 中的状态都变为 ready 时,这条指令才可能被发射到执行单元。
  3. Writeback (写回) 阶段:

    • add 指令执行完毕,计算出了结果。
    • 结果被写入 PhysRegFilep25 寄存器所在的位置。
    • 同时,写回阶段会立即通知 Scoreboard:“p25 现在已经 ready 了 (setReg(p25))”。
  4. 唤醒:

    • Scoreboardp25 的状态变为 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 的指令唤醒逻辑中。