跳转至

后端总览:从译码到精确提交

本文面向从事 RISC-V64 bring-up、性能分析和编译优化的软件工程师,目标不是逐行解释 RTL,而是建立一套可用于读性能现象的后端心智模型:一条指令在哪里获得物理寄存器、为什么会停在 Dispatch、何时真正完成,以及误预测、访存违例和异常如何恢复机器状态。

版本与阅读约定

  • 仓库:Daydream0929/XiangShan
  • 固定提交:c7b373b
  • 主配置:DefaultConfig。除“BackendV2Config 的区别”一节外,本文的宽度、容量和端口数都指该配置。
  • 源码事实表示能从固定提交直接确认的结构或条件。
  • 软件侧推导表示基于该结构得到的性能含义,仍需结合目标芯片计数器或仿真验证。
  • 待验证表示源码无法独立证明,或可能被生成参数、芯片版本、PMU 接线改变的结论。

不要把 Chisel 默认参数自动等同于手中芯片。首先确认芯片对应的生成配置和 RTL 提交,再使用本文中的具体数字。

一张图理解后端

flowchart LR
    FE["Frontend / IBuffer"] --> DEC["Decode\n标量译码与向量 uop 展开"]
    DEC --> REN["Rename\nRAT + FreeList + Snapshot"]
    REN --> DIS["Dispatch\nBusyTable + 资源门控"]
    DIS --> ROB["ROB / RAB\n顺序与恢复日志"]
    DIS --> IQ["Issue Queues\n按域等待操作数"]
    IQ --> RF["RF / RegCache / Bypass"]
    RF --> EXU["Integer / FP / Vector EXU"]
    RF --> MEM["Load / Store / VLSU"]
    EXU --> WB["Writeback Arbitration"]
    MEM --> WB
    WB --> ROB
    ROB --> COMMIT["In-order Commit\n精确异常"]
    EXU -. "branch/jump redirect" .-> REC["Redirect Generator"]
    MEM -. "load violation replay" .-> REC
    ROB -. "exception / interrupt / flush" .-> REC
    REC -. "恢复 RAT / FreeList / ROB / RAB / VType" .-> REN
    REC -. "杀死年轻 uop" .-> IQ

后端顶层实例化 CtrlBlock、整数/浮点/向量三个 Region,汇总执行结果和写回,并将最老 redirect 送回控制路径。Backend.scala:173-216 每个 Region 内包含 Issue Queue、DataPath、BypassNetwork、执行块和写回数据通路。Region.scala:38-120

DefaultConfig 的后端基线

Makefile 默认选择 DefaultConfigMakefile:50 DefaultConfig 组合默认 Tile、缓存和 LLC 配置。Configs.scala:534-539

项目 默认值 性能分析时代表什么
XLEN / VLEN / ELEN 64 / 128 / 64 标量宽度、向量寄存器位宽、向量元素上限
Decode / Rename / Commit 8 / 8 / 8 理论前端到退休带宽上限,不是持续 IPC 保证
ROB / RAB 352 / 352 在途顺序窗口 / 目的映射恢复日志;两者单位不同
VTypeBuffer 64 在途向量类型状态的重排序容量
Int preg 224,4 bank 整数物理寄存器容量与 bank 结构
FP preg 256 浮点物理寄存器容量
VF preg 128 向量/浮点向量数据物理寄存器容量
V0 / VL preg 22 / 32 mask 与向量长度状态分别重命名
标量 Load / Store 3 / 2 访存执行通路数量;还会受 LSQ、cache、TLB 限制
向量访存通路 2 其中一条承担 segment 类操作

参数来源:Parameters.scala:48-55Parameters.scala:85-153Parameters.scala:168-187

软件侧推导

  • 8-wide 只描述某些级的最大宽度。分支恢复、复杂向量指令展开、寄存器短缺、IQ/LSQ/ROB 满、数据依赖和写回冲突都可能降低实际 IPC。
  • ROB 和 RAB 都是 352 并不表示“一条指令恰好占两边各一项”。ROB 跟踪顺序完成;RAB 只为实际发生的目的寄存器映射记日志。
  • 向量状态不是隐藏的单一全局寄存器:V0、VL、向量目的寄存器和 vtype 都参与乱序跟踪,因此向量代码会制造标量代码没有的资源压力。

Decode:架构指令变成什么 uop

源码事实

DecodeStage 使用 8 个简单译码器,并用 DecodeUnitComp 处理复杂向量指令;它还定位本组中的第一条复杂向量指令。DecodeStage.scala:113-164

复杂向量指令产生的 uop 被放在简单标量结果之前,并受可用输出槽约束。DecodeStage.scala:219-246 Decode 会重排向量源,单独转换 V0,并断言一个 uop 最多写一个寄存器文件。DecodeStage.scala:247-265

向量译码生成 mask、old-vd、partial-vd、VL、vtype 和 uop 信息;非法指令检查还覆盖特权级、FS/VS、舍入模式和部分 CBO 约束。DecodeUnit.scala:905-948 DecodeUnit.scala:1101-1168

vtype 恢复期间,Decode 可能不接受新的向量工作;本级提供 stall_cyclerecovery_bubble、输入 valid/fire 和融合指令计数。DecodeStage.scala:166-206 DecodeStage.scala:423-450

软件侧推导

  • 一条 RVV 指令可能占用多个内部 uop 槽,因此同样的架构指令数不能直接与标量代码比较 decode 压力。
  • 频繁改变 vtype 或在错误路径上执行向量状态更新,会放大恢复气泡。
  • old-vd 是否形成真实依赖取决于 VL、tail/mask policy;不要只从汇编中的目的寄存器名字推断依赖图。

Rename:消除假依赖并建立恢复点

RAT、FreeList 与 move elimination

Rename 为整数、FP、VF、V0、VL 分别维护映射和空闲物理寄存器。整数使用支持 move elimination 的 MEFreeList,其他域使用 StdFreeListRename.scala:114-124

RenameTable 同时维护 speculative table 和 architectural table。redirect 时,speculative table 可从选中的 snapshot 或 architectural table 恢复;提交更新 architectural table,并计算可释放的旧物理寄存器。RenameTable.scala:105-177

整数 move 可令新目的直接复用源物理寄存器,不再分配新寄存器;同一 rename 组内还会逐 lane 旁路刚建立的映射。Rename.scala:684-759

Rename 输出按组门控:只要本组需要的任一 FreeList、LSQ 或下游 Dispatch 资源不足,整组就不能前进。Rename.scala:264-305 Rename.scala:450-580

Snapshot 的准确边界

默认只配置 4 个 rename snapshot;生成 snapshot 的间距门限为 RobCommitWidth * 4,默认即 32 个 ROB 项。当前触发条件明确写成 FuType.isJump 且要求 first uop,并不是每条条件分支都创建快照。Rename.scala:777-805

软件侧推导

  • rename_stall_cycle_*Fl 表示物理寄存器生命周期压力,不等同于编译器 spill。大展开、大 LMUL、长依赖链和较晚提交都可能让旧映射迟迟不能回收。
  • V0/VL/Vec FreeList 短缺可以连带阻塞同一 rename 组里的标量 uop。
  • move elimination 能减轻整数物理寄存器压力;是否真正生效应以 RTL 计数器或仿真为准,不能仅看到 mv 伪指令就下结论。

Dispatch:有空位不代表能进入执行队列

源码事实

Dispatch 为 Int、FP、VF、V0、VL 维护 BusyTable。分配物理目的时置 busy,写回或有效 wakeup 清除 busy,load cancel 等失败路径可重新置 busy。Dispatch.scala:242-360 BusyTable.scala:144-176

向量 old-vd 在 VL、VTA、VMA 满足条件时可以被判定为无需等待。Dispatch.scala:363-384

Dispatch 会在兼容 IQ 间比较占用并选择较空者;若发生 stall,则保持原选择,防止 uop 在队列间来回摆动。Dispatch.scala:412-559

Load、Store 和向量访存还必须通过每周期结构限制以及 LQ/SQ 容量检查。Dispatch 保持程序顺序,只有实际 fire 的 uop 才会进入 ROB。Dispatch.scala:667-708 Dispatch.scala:835-879

可观察的 stall 分类包括 ROB、IQ、LSQ、FreeList、各执行类型和各 IQ 类型。Dispatch.scala:889-899 Dispatch.scala:1029-1082

ROB 与 RAB:完成、提交和恢复不是一回事

flowchart TD
    UOP["uop dispatched"] --> ROBE["分配 ROB entry"]
    UOP -->|"有真实目的映射"| RABE["写 RAB mapping log"]
    ROBE --> EXEC["执行 / replay / 写回"]
    EXEC --> DONE["ROB completion tracking"]
    DONE --> HEAD{"到达 ROB head?"}
    HEAD -->|否| WAIT["保持乱序完成状态"]
    HEAD -->|是,正常| RETIRE["顺序提交并释放旧映射"]
    HEAD -->|是,异常/中断/replay| FLUSH["生成精确 flush"]
    FLUSH --> WALK["ROB + RAB + VType walk"]
    WALK --> RESUME["恢复取指与重命名"]

ROB

ROB 为 352 项、8 bank。它能否接收新工作不仅取决于 ROB 自身,还取决于 RAB、VTypeBuffer 和向量异常合并资源。Rob.scala:164-209

ROB 记录 uop 的完成数,并按程序顺序提交。异常、中断和 replay 只有在相关项到达 ROB 头且满足完成条件时才成为架构可见事件。Rob.scala:607-699 Rob.scala:771-820

RAB

RAB 的一项对应一次实际目的映射,而不是必然对应一条指令。提交时它把旧、新映射送回 Rename;恢复时按相反方向 walk。Rab.scala:34-64 Rab.scala:215-274

有可用 snapshot 时,RAB 可以从快照位置直接进入 walk;没有快照时先执行 special_walk。ROB 只有等待 ROB、RAB、VType 三者的恢复都完成后才回到 idle。Rob.scala:870-955

精确异常与向量 vstart

ExceptionGen 先按 ROB 顺序选择最老异常;若多个异常属于同一 ROB 项,再根据 vuopIdx/vstart 保留最早的向量异常。ExceptionGen.scala:49-175 向量 load 异常还会等待必要的 RAB 映射提交。Rob.scala:621-639

软件侧推导: 即使年轻指令已经执行完,也不能越过 ROB 头异常提交。性能采样看到某条年轻指令“已完成”并不代表它已退休。RVV 异常调试应同时检查 vstart、faulting element、目的映射恢复和 ROB 头状态。

Redirect、恢复与 replay 的三种语义

Redirect 携带 ROB 位置、flush 级别、目标、误预测以及调试分类;多个候选中选择程序顺序最老者。Bundle.scala:211-287

RedirectGenerator 在执行单元 redirect 与 load replay 间选择最老者,过滤已经被更老 redirect 冲刷的请求;ROB flush 具有更高优先级。RedirectGenerator.scala:27-61 CtrlBlock 随后杀死错误路径写回,并选择不晚于 redirect 的最近有效 snapshot 恢复 RAT、FreeList、ROB 和 RAB。CtrlBlock.scala:109-151 CtrlBlock.scala:557-604

调试时必须区分三类“重试”:

类别 是否重定向前端 典型触发 观察位置
Issue/operand retry RF 仲裁失败、load cancel、OG 阶段未成功 og0Failedog1Failed、IQ finalSuccess
Load pipeline replay 通常否 cache/TLB、bank conflict、数据未就绪、RAW/RAR 等 load replay cause、fast/high/low replay
Load ordering violation 已执行 load 被发现违反内存顺序 loadReplay redirect、恢复周期

IQ 的早期失败反馈见 DataPath.scala:608-648。Load pipeline 的 replay 仲裁与计数见 NewLoadUnit.scala:95-155NewLoadUnit.scala:482-500。Replay 原因枚举见 LoadQueueReplay.scala:31-72。内存顺序违例形成 CtrlBlock loadReplay 的路径见 CtrlBlock.scala:208-213

当前提交保留 ROB replayInst 基础设施,但源码搜索未发现默认 FU 把 replayInst 置真;因此它应列为待验证/扩展路径,而不是默认活跃的第四类 replay。

面向软件性能工程师的诊断顺序

先确定“窗口为什么不能退休”,再追执行端。单看 IPC 或单个 cache miss 计数通常不足以定位后端瓶颈。

症状 第一组证据 第二组证据 可能的软件动作
ROB 长期接近满,提交低 ROB occupancy、head wait 类型 对应 IQ、load replay、cache/TLB 缩短关键链;改善局部性;检查异常/序列化指令
Rename 停顿 Int/FP/Vec/V0/VL FreeList stall ROB 提交与物理寄存器利用率 减少展开和 live range;调整 LMUL;帮助编译器缩短变量生命周期
分支恢复气泡高 BR_MIS_PREDTOTAL_FLUSH recovery bubble、walk cycle、snapshot 使用 改善热路径布局;降低难预测控制流;评估 if-conversion
Load replay 高 各 replay cause LQ/SQ、cache/TLB、RAW/RAR 减少地址别名;改善对齐/局部性;检查 store-to-load 模式
向量 IPC 低 Vec/V0/VL FreeList、VecIQ、VLSU stall old-vd、RF 端口、replay 调整 LMUL/展开;减少无谓 mask/merge 依赖;改善向量访存对齐

ROB 提供 commit IPC/CPI、ROB head wait、各 FuType 的 dispatch/select/issue/execute/commit 延迟分解、walk 和 snapshot 使用统计。Rob.scala:1324-1426 分支误预测、flush、异常、replay 和 ROB 占用统计见 Rob.scala:1693-1740

待验证: XSPerf 名称是否映射到量产芯片的 mhpmevent 或 Linux perf 事件,取决于生成配置、PMU 接线、特权软件和芯片版本。源码中存在计数器,不等于用户态一定能直接读取。

DefaultConfig 与 BackendV2Config 的区别

BackendV2Config 继承 DefaultConfig 后覆写参数,并带 DeprecatedConfigWarning;它不是本提交 Makefile 的默认选择。Configs.scala:447-490

参数 DefaultConfig BackendV2Config
EnableBackendV2Config false true
Dispatch IQ balance true false
IBuffer NumReadBank 基础配置值 6
DecodeWidth 8 6
RenameWidth 8 6
RabCommitWidth 8 6
CommitWidth / RobCommitWidth 8 未覆写,仍为 8
ROB 352 208
RAB 352 256
Int preg 224,4 bank 224,1 bank
FP preg 256 192,1 bank
VF preg 128 128,1 bank
V0 / VL preg 22 / 32 22 / 32,均 1 bank

EnableBackendV2Config=true 时,后端改用 BackendV2SchdParams 的整数、FP、向量 scheduler 和 wakeup 拓扑。Parameters.scala:503-517 因此不能把 V2 的执行单元数量、RF 端口编号或 IQ 深度混入 DefaultConfig 分析。

已知待验证点

  1. 实际 CPU 使用的生成配置、提交和后续 patch 是否与 c7b373b + DefaultConfig 一致。
  2. Rename.scala:1057-1059 对 Vec/V0/VL 使用了重复的内部累计名称 stall_cycle_vec,而命名事件在后文分开,需在生成 RTL/仿真中确认最终计数器行为。源码
  3. Dispatch.scala:899 的 LSQ stall 条件使用 !lsqCanAccept,而 dispatch_stall_cycle_lsq 事件处使用 lsqCanAccept,命名与条件疑似不一致,不能未经验证直接用于归因。源码一 源码二
  4. Load replay 枚举中的 C_WFC_MF 在当前 NewLoadUnit 路径未见有效置位;应先视为保留原因,再用波形或生成 RTL确认。
  5. rename snapshot 只由 FuType.isJump 触发是本提交的源码事实;其微架构设计意图及量产 RTL 是否保持该策略仍需验证。