香山处理器 LSU 模块深度解析
作者:Xiao Hong 🌸
日期:2026-05-05
一、概述
LSU(Load-Store Unit,存取单元)是处理器前端与内存子系统之间的桥梁,承担着所有内存访问操作的发起、协调与完成。香山(XiangShan)处理器的 LSU 是一个高度复杂的模块,包含了完整的 5 级流水线、LSQ(Load-Store Queue)队列管理、存储转发(Store-to-Load Forwarding)、乱序执行支持、内存依赖预测、以及向量存取扩展等一系列机制。
本文将自顶向下、从宏观到微观,对香山 LSU 的各个子模块进行系统性的剖析。
声明:本文基于香山开源代码(kunminghu-v3 分支)编写,部分细节可能随版本迭代而变化。建议结合源代码
src/main/scala/xiangshan/mem/目录共同阅读。
二、整体架构:MemBlock
MemBlock(MemBlock.scala)是 LSU 乃至整个内存子系统的中央调度器。它实例化了所有存取执行单元、队列、缓存和 TLB 子系统,并将它们通过大量信号线连接在一起。
2.1 角色定位
MemBlock 位于乱序后端(OoO Backend)和 L1 DCache / Uncache 之间。它的上游是从 Issue Queue 发来的指令,下游是 TileLink 总线通往 DCache 和 Uncache。整体上看:
1 | Issue Queue ──▶ MemBlock ──▶ DCache / Uncache |
2.2 核心子模块
MemBlock 内部实例化的子模块非常丰富:
| 模块 | 数量 | 职责 |
|---|---|---|
NewLoadUnit |
LduCnt | 5 级标量加载流水线 |
NewStoreUnit |
StaCnt | 5 级标量存储地址流水线 |
StdExeUnit |
StdCnt | 存储数据写回单元 |
AtomicsUnit |
1 | LR/SC/AMO 原子操作 FSM |
VLSplitImp / VSSplitImp |
各多个 | 向量存取元素拆分单元 |
VLMergeBufferImp / VSMergeBufferImp |
各多个 | 向量存取合并缓冲 |
VSegmentUnit |
多个 | 向量分段指令单元 |
VfofBuffer |
多个 | 向量 First-Over-Fault 缓冲 |
LsqWrapper |
1 | 统一的 LSQ 封装(LoadQueue + StoreQueue) |
Sbuffer |
1 | 存储合并缓冲 |
PrefetcherWrapper |
1 | L1 预取器 + L2 预取接口 |
TLBNonBlock |
3 | 非阻塞 DTLB(dtlb_ld / dtlb_st / dtlb_prefetch) |
L2TLBWrapper |
1 | 页表巡游器(L2 TLB) |
DCacheWrapper |
1 | L1 数据缓存 |
Uncache |
1 | 不可缓存(MMIO/NC)访问单元 |
PMP / PMPChecker |
各多个 | 物理内存保护 |
2.3 Uncache 仲裁 FSM
MemBlock 内部维护了一个 3 状态 FSM 来仲裁 Load 和 Store 的 Uncache 请求:
s_idle:空闲态,同时监听 load 和 store 请求,按 ROB 顺序公平选择s_load:正在处理 Uncache 加载s_store:正在处理 Uncache 存储
当两个请求同时到达时,用 robIdx 排序决定优先级,避免饥饿。
三、Load 流水线:NewLoadUnit
NewLoadUnit 是香山 LSU 中最复杂的模块之一,实现了 5 级(Stage 0~4)的有序加载执行流水线。
3.1 流水线各阶段
Stage 0(S0):请求仲裁
S0 负责从多个请求源中选择最高优先级的一个,并发起 TLB 和 DCache 请求。请求源的优先级顺序(由高到低)为:
1 | unalignTail > replayHiPrio > fastReplay > replayLoPrio |
S0 会同时向以下模块发送请求:
- DTLB:发起虚拟地址翻译请求
- DCache:发起缓存访问请求
- SQ(Store Queue):请求存储转发数据
- Sbuffer:请求存储合并缓冲转发
- Uncache / MSHR / TLD:其他转发源
Stage 1(S1):地址翻译与一致性检查
S1 从 TLB 获取物理地址,并执行以下操作:
- Load Trigger 检查:检查触发器条件
- Nuke 查询:向所有 Load 单元发送 RAW 冲突查询(store-to-load 冲突)
- 未对齐尾部注入:如果加载跨越 16B 边界,将尾部注回 S0 重新处理
- 写入 LSQ:将地址信息写入 Load Queue
Stage 2(S2):转发与 Replay 原因生成
S2 是最关键的阶段:
- PMP 检查:验证物理地址的访问权限
- 数据转发汇聚:从 SQ、Sbuffer、Uncache、MSHR、TLD 和 DCache 多路收集转发数据
- Replay 原因生成:根据 13 种优先级有序的 Replay 原因生成错误标志
Stage 3(S3):写回与快速重放
S3 完成实际的数据写回:
- 向后端写回
LsPipelineBundle - 向 LSQ 写入完整的加载信息
- 为 Store-Load 冲突生成 Rollback 重定向
- 撤销 RAR 条目
- 触发 Fast Replay(不需要完整 LSQ 重新插入的快速重放)
Stage 4(S4):未对齐数据拼接
简单的透传阶段,负责将跨越 16B 边界的加载的头部和尾部数据拼接在一起。
3.2 数据通路的 3 级流水
Load 单元还有一个独立于控制流水的数据通路(LoadUnitDataPath),它本身也是 3 级流水:
- 第 1 级:收集各转发源的数据
- 第 2 级:对齐数据
- 第 3 级:向写回阶段交付数据
3.3 未对齐加载处理
当加载操作跨 16B 边界时,会拆分为:
- Head 部分:在 S0 发起
- Tail 部分:在 S1 通过
unalignTail端口注回 S0 重新处理 - S4 将两部分数据拼接后输出
3.4 Fast Replay 机制
S3 可以直接将某些 Replay 场景(Bank Conflict、Replay Carry 等)通过 fastReplay 路径发回 S0,而不需要经过 LoadQueueReplay 的完整调度流程。这种机制大幅降低了轻量级冲突的重放延迟。
四、Store 流水线:NewStoreUnit
NewStoreUnit 实现 5 级有序存储地址流水线,主要负责地址生成、翻译和 Store Queue 的写入。
4.1 流水线各阶段
Stage 0(S0):地址生成与检查
- 计算
vaddr = src(0) + imm - 检查对齐:
isUnalign、跨 16B 检查、跨 4K 页检查 - 发送 TLB 请求
Stage 1(S1):TLB 响应与 Nuke 查询
- 接收 TLB 响应
- 处理 Store Trigger
- 向所有 Load 单元发送 Nuke 查询请求(RAW 冲突检测)
- 将地址写入 StoreQueue(
toSqAddr) - 更新 LFST(清除预测的 Store)
- 将未对齐尾部注回 S0
Stage 2(S2):PMP 检查与异常处理
- 接收 PMP 响应
- 检测异常(访问fault、地址错位)
- 生成内存类型标识(Cacheable / NC / MMIO)
- 通过
toSqAddrRe重新写入 SQ(此时已知 paddr) - 发送预取训练信号
Stage 3(S3):写回
- 向 ROB 写回(标量通过
io.stout) DelayPipeline延迟向量存储写回(RAW 同步)- 拼接未对齐的头部和尾部
Stage 4(S4):未对齐拼接
透传阶段,同 Load 流水线的 S4。
4.2 未对齐存储
与未对齐加载类似,跨 16B 的存储被拆分为头部和尾部:
- 尾部:在 S1 通过
unalignTail注回 S0 - 第二地址:存入
UnalignQueue(SQUnalignQueueSize个条目)
4.3 CBO 操作
CBO(Cache Block Operation,如 cbo.clean/flush/inval/zero)作为特殊 Store 类型处理,在地址已知的情况下可以绕过 TLB。
五、存储数据单元:StdExeUnit
StdExeUnit(StdExeUnit.scala)是纯组合逻辑的存储数据调度单元,负责将标量和向量存储数据路由到正确的目标。
5.1 核心机制
- 仲裁逻辑:
io.in.ready := !io.vstdIn.valid,即向量存储数据优先于标量 - ROB 写回:仅在
scalar && !vector && !isAMO时有效 - AMO 数据转发:当
FuType.storeIsAMO为真时,数据通过io.atomicData转发到AtomicsUnit - SQ 数据写入:向量和标量存储数据通过
io.sqData写入 StoreQueue,向量优先
六、原子操作单元:AtomicsUnit
AtomicsUnit 是一个 11 状态的 FSM,负责处理 RISC-V 的 LR(Load-Reserved)、SC(Store-Conditional)和 AMO(原子内存操作)指令。
6.1 状态机
| 状态 | 主要操作 |
|---|---|
s_invalid |
空闲 |
s_tlb_and_flush_sbuffer_req |
并行发送 TLB 请求和 Sbuffer 刷新请求 |
s_pm |
PMP 检查 |
s_wait_flush_sbuffer_resp |
等待 Sbuffer 刷新完成 |
s_cache_req |
发送 DCache 原子访问请求 |
s_cache_resp |
接收 DCache 响应 |
s_cache_resp_latch |
锁存数据 |
s_finish / s_finish2 |
写回(部分 AMO 需要多次写回) |
s_extra_wb / s_extra_wb2 |
AMOCAS.Q 的额外写回(4 个 std uops,2 个 sta uops) |
6.2 关键设计
- SLR/SC 保证:LR 会预留一个缓存行(设置有效位),SC 只有在该行仍然reserved 时才能成功
- Sbuffer 刷新:在发起 DCache 原子访问前,必须确保 Store Buffer 已排空(否则可能看到旧数据)
- AMOCAS.Q:64 位双字 CAS 需要 4 个 std uops 和 2 个 sta uops,产生 2 次寄存器写回
七、LSQ 封装:LsqWrapper
LsqWrapper(LSQWrapper.scala)将 LoadQueue 和 NewStoreQueue 封装为统一接口,负责指令入队、Uncache 仲裁和所有流水线信号的路由。
7.1 核心接口
1 | io.enq ← Dispatch(入队) |
7.2 Uncache 仲裁
同 MemBlock,Load 和 Store 的 Uncache 请求在 LsqWrapper 内部也经过一个 3 状态 FSM 仲裁,使用 robIdx 保证顺序。
八、LoadQueue 体系
LoadQueue 并非单一模块,而是由多个协同工作的子模块组成:VirtualLoadQueue、LoadQueueRAR、LoadQueueRAW、LoadQueueReplay 和 LoadQueueUncache。
8.1 VirtualLoadQueue
跟踪所有已分配、在飞行中、已提交的加载指令的循环队列。管理 LQ 指针算术,并向 Dispatch 单元提供分配/释放状态。
关键机制:
- 入队:在
enqPtrExt处分配,enqPtrExt按接受条目数推进 - 写回:S3 的
ldin.fire触发committed标志设置 - 出队:
DeqPtrMoveStride = CommitWidth,ROB 提交时推进 - Redirect 恢复:通过
lqCancelCnt计算并清除已取消条目
8.2 LoadQueueRAR(Load-After-Load 冲突检测)
使用 16 位 XOR 哈希的部分物理地址 CAM 来检测 RAR 冲突:
- 低 5 位:
paddr[6+i] XOR paddr[15-i] - 高 11 位:
paddr[i+6] XOR paddr[i+17] XOR paddr[i+28] XOR paddr[i+39]
当 S2 的加载查询命中一个更旧的、未释放的加载条目时,触发 RAR 冲突并撤销该条目。
8.3 LoadQueueRAW(Store-Load RAW 冲突检测)
使用 24 位部分物理地址 CAM 进行更精确的 RAW 冲突检测:
- 当 StoreUnit S1 写入新存储地址时,触发对 LoadQueueRAW 的 CAM 查询
- 命中的条目经过 多周期最旧条目选择(
SelectOldest递归优先级选择器) - 冲突时产生
rollback重定向,并发送mdpTrain到前端更新 StoreSet 预测器
8.4 LoadQueueReplay(重放调度)
负责将需要重新执行的加载调度回 Load 流水线 S0,核心机制包括:
3 级重放流水线
1 | S0: 选择 |
13 个 Replay 原因(优先级从高到低)
| 优先级 | 原因 | 描述 |
|---|---|---|
| 0 | C_UNCACHE | 不可缓存访问 |
| 1 | C_SMF | 存储多路转发无效 |
| 2 | C_MA | St-Ld 冲突重新执行检查 |
| 3 | C_TM | TLB 未命中 |
| 4 | C_FF | 存储到加载转发检查 |
| 5 | C_DR | DCache 重放 |
| 6 | C_DM | DCache 未命中 |
| 7 | C_WF | 写回单元预测失败 |
| 8 | C_BC | 银行冲突 / 未对齐尾部拆分失败 |
| 9 | C_RAR | RAR 队列接受检查 |
| 10 | C_RAW | RAW 队列接受检查 |
| 11 | C_NK | St-Ld 冲突 nuke |
| 12 | C_MF | 未对齐缓冲满 |
⚠️ 优先级顺序对死锁预防至关重要,代码中明确警告不可随意调整。
冷启动机制(Cold-Down)
DCache 未命中的条目进入冷启动计数器(默认 16 周期),冷却期满后才重新考虑重放。L2 Hint 可以提前唤醒冷启动中的条目。
8.5 LoadQueueUncache
管理 MMIO/NC 加载事务的每条目 5 状态 FSM:
1 | s_idle → s_req → s_resp → s_wakeup → s_wait |
- MMIO:写回必须等待 ROB 提交顺序保证
- NC:直接写回,无 ROB 顺序约束
九、StoreQueue 体系:NewStoreQueue
NewStoreQueue 管理从入队到完成的全部存储指令流,包含三个核心子模块。
9.1 ForwardModule(存储到加载转发)
3 级流水线负责对每个加载查询返回正确的转发数据:
Stage 0:
- 生成加载字节起止位置、
ageMask、转发掩码 - Store-Set 命中检测(LFST 启用时比较 robIdx,否则比较 SSID)
Stage 1:
- 虚拟地址匹配(忽略低 6 位 VWordOffset)
- 跨 16B 处理(
s1Same16BMatchVec/s1Next16BMatchVec) - 字节重叠检查:
storeRangeStart <= loadRangeEnd && storeRangeEnd >= loadRangeStart - 两段最年轻选择(
findYoungest递归优先级选择器)
Stage 2:
- 物理地址匹配检查
- 数据旋转:
rotateByteRight将存储数据移位对齐到加载位置 - 转发无效条件检测:
s2PaddrNoMatch、s2Cross4KPage、s2MultiMatch(无完全覆盖)
9.2 DeqModule(出队管理)
处理四类存储的出队逻辑:
- 可缓存存储:格式化为
WriteToSbufferReqEntry发往EnterSbufferQueue,跨 16B/跨页时拆分 - NC/MMIO 存储:经 Uncache 仲裁器发往 Uncache 缓冲
- CBO 操作:经 CMO 接口发往 DCache
- 强制写回:
forceWrite时(sbuffer 接近满),加速写入 sbuffer
9.3 EnterSbufferQueue
StoreQueue 和 Sbuffer 之间的顺序写缓冲,确保在 Sbuffer 单周期无法接受全部 EnsbufferWidth 条目时仍能维持顺序性(环形队列,FIFO 顺序)。
9.4 UnalignQueue
存储跨页或未对齐存储的第二物理地址,条目格式为 (paddrHigh, robIdx, sqIdx)。在出队时由 DeqModule 读取,用于补全跨页存储的第二个物理地址。
十、存储缓冲:Sbuffer
Sbuffer 是提交存储与 L1 DCache 之间的一级写合并缓冲,采用多状态 FSM 管理。
10.1 顶层状态机
1 | x_idle ← 正常操作,接受 SQ 写入,执行 DCache 写回 |
10.2 核心机制
- 写合并(Write Merging):收到写请求时,先检查是否存在相同 ptag 的条目。命中则合并(掩码 OR,新数据覆盖重叠字节);未命中则按 PLRU 分配新条目
- DCache 写流水线(3 级):
- Out S0:从 SbufferData 读取条目数据
- Out S1:向 DCache 发送写请求
- Out extra:处理 DCache 写命中响应,清理条目
- 转发(Forwarding):负载可从 Sbuffer 条目直接获取数据(VTag 匹配)。但如果 vtag 与实际 paddr 不匹配,则触发微架构排空(evacuate)——将冲突条目 evacuate 到 DCache
- 一致性超时:
cohCount计数器追踪条目的有效时长,超时强制写回 DCache 以防止饥饿 - PLRU 替换:伪 LRU 树用于在 Sbuffer 满时选择淘汰候选
十一、内存依赖预测:StoreSet / SSIT / LFST / WaitTable
11.1 SSIT(Store Set 标识表)
SRAM 实现的 Store Set 标识表:
- 读端口:DecodeWidth(decode 读取和 update 逻辑共用)
- 写端口:2 个(Port 0 = load 更新 + flush,Port 1 = store 更新)
- 4 种更新情况覆盖 load 和 store 的 SSID 分配/继承/竞争
11.2 LFST(最后取指存储表)
每个 SSID 一个 FIFO 表:
- Dispatch/Rename:记录该 SSID 下最近取指的 Store
- Store 发射:清除对应 LFST 条目中的预测
- Redirect 恢复:LFST 在分支 mispredict 等重定向时恢复
11.3 WaitTable
Alpha 21264 风格的 Load 等待预测表,使用 2 位饱和计数器:
- 读:
loadWaitBit = (counter[1] || csrCtrl.no_spec_load) && !csrCtrl.lvpred_disable - 更新:violation 时向饱和方向(11)计数
- 超时重置:防止计数器永久饱和
十二、向量化扩展
香山 LSU 包含了丰富的向量存取支持:
- VSplit / VSSplit:将向量存取指令拆分为独立的元素级访问
- VLSplitImp / VSSplitImp:元素拆分与分发
- VLMergeBufferImp / VSMergeBufferImp:元素合并缓冲
- VSegmentUnit:向量分段指令(segment load/store)
- VfofBuffer:向量 First-Over-Fault 缓冲(用于处理向量访问中的首个错误元素)
向量存储数据流:vsSplit → stdExeUnits.io.vstdIn → lsq.io.std.storeDataIn
十三、关键设计哲学
13.1 优先级有序的冲突处理
LoadQueueReplay 中的 13 个 Replay 原因严格按优先级编码,这个顺序是死锁安全的核心。改变优先级可能导致死锁,因此在代码中有明确的警告注释。
13.2 多级转发
香山 LSU 实现了从多个来源的数据转发路径:StoreQueue、Sbuffer、Uncache、MSHR(Miss Status Holding Register)、TLD(Top-Level Directory),以及最终的 DCache。优先级和选择逻辑分布在多个流水线阶段。
13.3 解耦设计
存储端的 Store Queue、EnterSbufferQueue 和 Sbuffer 形成三级缓冲,每级都可以独立进行握手和反压,实现了乱序执行后端和解耦的 DCache 写入管道之间的有效隔离。
13.4 预测与投机
WaitTable 和 StoreSet 预测器使 Load 可以投机性地提前执行,绕过未完成的 Store 依赖。当预测失败时,通过 Rollback 重定向和 MDP 训练来纠正。
十四、总结
香山处理器的 LSU 是一个工程复杂度极高的内存子系统,其设计融合了现代处理器内存层次管理的最佳实践:
- 乱序执行:通过 LSQ 体系(VirtualLoadQueue、StoreQueue)与 Replay 机制支持乱序发射和内存顺序保证
- 低延迟访问:多级存储转发(Sbuffer → SQ → DCache)尽量减少加载延迟
- 内存一致性:通过 RAR/RAW 队列在流水线内部完成冲突检测,避免深度的重放链
- 原子操作:专用 11 状态 FSM 处理 LR/SC/AMO,确保 RISC-V 内存模型的正确性
- 向量扩展:完整的向量存取流水线支持,满足 RISC-V Vector 扩展需求
- 可预期性:全面的性能计数器、TLP(Top-Level Performance)接口和调试信号支持
理解 LSU 的每个子模块及其交互,是掌握香山处理器微架构的必经之路。
参考资料
- 香山源代码:
src/main/scala/xiangshan/mem/ - 架构文档:
docs/mem-block-spec.md - RISC-V 架构规范
本文由 Xiao Hong 🌸 基于开源代码撰写,如有疏漏欢迎指正。
