LSU(Load/Store Unit)—— 访存流水线的命门
承 superscalar-design §1.4 的 LSU 初步介绍和 store-buffer 的 store buffer 详解。LSU 是 CPU 和内存子系统之间的唯一通道——所有 load/store 指令经此翻译地址、排队、访问 D-Cache 和更外层存储。LSU 内部的 AGU 数量直接决定了密集访存代码的 IPC 上限。
一、LSU 解决什么问题
load/store 跟其他运算指令(ADD、FMA)有本质区别:它们不是"拿到操作数 → 算一算 → 出结果",而是"拿到操作数 → 算地址 → 去内存系统里读/写数据"。地址计算(AGU 阶段)和实际访存(D-Cache 访问)是两个独立步骤,且访存延迟远高于运算延迟(L1 hit ~4-5 拍,L3 miss ~40+ 拍,DDR miss ~200+ 拍)。
如果把 load/store 和其他 ALU 指令硬塞在同一个执行流水线里,访存延迟会拖死所有运算。所以现代 CPU 单独设立 LSU,把访存路径从运算流水线中抽出来。
二、功能拆解
| 部件 | 做什么 | 并行度/容量 |
|---|---|---|
| AGU(地址生成单元) | load/store µop 到达后,计算访存地址:base + index × scale + offset | 3~4 个 AGU,同一拍最多并行生成 3~4 个访存地址 |
| Store Buffer | store 指令不直接写 D-Cache,而是写入 store buffer 后从流水线释放;等指令在 ROB 中退休后,LSU 异步刷入 D-Cache | 几十个条目 |
| Load Buffer | 跟踪正在飞行的 load 请求,包括等 L2/L3/内存响应的 cache miss | 几十个条目 |
| Store→Load Forwarding | 当 load 地址与 store buffer 中某条未退休 store 地址匹配时,LSU 直接把 store buffer 中的数据旁路给 load | 硬件地址匹配逻辑 |
Store Buffer 的深入分析见 store-buffer。本篇聚焦 LSU 整体和 AGU 的访存调度机制。
三、运作机制
3.0 AGU(地址生成单元)详解
AGU 是 Address Generation Unit(地址生成单元) 的缩写。它是 LSU 内部的专用硬件,负责为每条 load/store 指令计算目标内存地址。本节把 AGU 从 LSU 的功能拆解表中单独拎出来讲清楚,因为它是整个访存流水线的第一道关口——AGU 数量直接决定密集访存代码的 IPC 上限。
AGU 算什么东西
所有需要访问内存的 x86 指令(mov [rax], rbx、add qword [rsi+8], rcx 等),其目标地址由 四段拼成:
有效地址 = base + index × scale + offset
base — 基址寄存器(如 RAX、RBP),必须
index — 变址寄存器(如 RSI),可选
scale — 缩放因子(1、2、4、8),用于数组寻址
offset — 立即数偏移(如 0x10 或 -16)AGU 每拍做一次 加法和移位:R[base] + (R[index] << scale) + sign_extend(offset)。
为什么 AGU 要跟 ALU 分开
| ALU | AGU | |
|---|---|---|
| 算什么东西 | 数据:rax + rbx | 地址:[rsi + rax*8 + 0x10] |
| 结果送给谁 | 写回 PRF → 寄存器 | 发给 LSU → D-Cache Tag 阵列 |
| 关键约束 | 加法 1 cycle | 也是 1 cycle,但后面还要接 D-Cache 查 Tag + 读数据 ~4~5 cycle |
| 并行需求 | 每拍可以有 4 条 ADD | 每拍最多 3 个 AGU 操作(2 load + 1 store) |
分开有两个原因:
- 端口隔离:如果把地址计算和 ALU 运算混在一起分到同一个端口上,一条长延迟指令(如 DIV)会把跟它无关的 load/store 也堵住——即使 load 的基址寄存器早就就绪了。AGU 有独立端口后,load/store 只跟其他 load/store 抢 AGU,不和 ALU 抢。
- 流水线分叉:ALU 的结果下一拍进 MEM 级透传就完了;AGU 算完地址后要走完全不同的路径——TLB 翻译 VA→PA、store buffer 前向查找、D-Cache Tag 比较、数据读取 / 写入——这些物理上都在 LSU 和 D-Cache 流水线里,跟 ALU→PRF 写回路径不走同一套硬件。
AGU 端口分配(以 Skylake 客户端为例)
Port 2 → Load AGU #0 每拍 1 个 load 地址
Port 3 → Load AGU #1 每拍 1 个 load 地址
Port 7 → Store AGU 每拍 1 个 store 地址
总 AGU 带宽 = 3 个地址/拍(2 load + 1 store)Port 4 也参与 store——Port 7 算地址、Port 4 同时把 store 数据从 PRF 写到 store buffer。但 Port 4 不算 AGU,它归在 store data 流水线里。
这就是密集访存代码的硬上限:不管你有 8 个 ALU + 4 个 FPU,每拍最多只能发 2 个 load + 1 个 store。如果一次循环需要 3 个 load + 2 个 store(像 -O0 编译的 sum += i),光消化 AGU 占用就得至少 ceil(3/2) + ceil(2/1) = 4 cycle。
AGU 在保留站→执行→LSU 链中的位置
保留站(RS) 执行级(EX) 访存级(MEM)
┌──────────┐ 发射 ┌──────────────┐ ┌──────────────────┐
│ LOAD µop │ ────────→ │ AGU 算地址 │ ──────→ │ LSU: │
│ 等待 base │ Port 2/3 │ base+idx×s+off│ │ TLB→SB查找→DCache│
│ 寄存器就绪│ │ 输出 PA/VA │ │ 命中 → 数据→WB │
└──────────┘ └──────────────┘ └──────────────────┘
↑ µop 发射后立即释放 RS 槽位
AGU 本身 1 cycle关键时序:µop 在 RS 里等的是基址寄存器就绪(比如 [RBP-16] 等 RBP),不是等 D-Cache 返回数据。AGU 算地址只要 1 cycle,算完就把 µop 交给 LSU。之后 D-Cache 的 ~4~5 cycle 等待发生在 LSU 的 Load Buffer 里,不占保留站、不占 AGU。
LEA 的特殊性:不进 AGU
lea rax, [rbx + rcx*4 + 8] 虽然写法上用了内存寻址语法 [...],但它不访问内存——CPU 把它当成纯 ALU 操作,走 ALU 端口(Port 0/1/5/6),不走 AGU。编译器用 LEA 实现 (b + c*4 + 8) → rax 这种多操作数加法,就是为了绕开 AGU 端口瓶颈。这是 -O2 优化后 IPC 飙升的关键手段之一。
一句话:AGU 是"管地址不管数据"——它只管把
[寄存器 + 寄存器×系数 + 偏移]算出一个最终地址,然后把这个地址交给 LSU。AGU 个数(3~4)是访存带宽的硬天花板,-O2用 LEA 绕开 AGU 是 IPC 翻倍的关键。
3.1 Load 路径
① 发射 — 保留站把 load µop 发射到 LSU
② AGU — 计算访存地址(如 rax+rsi*8+0x10)
③ 翻译 — TLB 把 VA 翻译成 PA(TLB miss → page walk,[tlb](/concepts/cache/tlb.md))
④ 检查 — 查找 store buffer 中是否有同一地址的未退休 store
├── 命中 → store→load forwarding,直接返回数据
└── 未命中 → 继续
⑤ 访问 — 查 L1 D-Cache
├── L1 hit → 4~5 拍返回数据
├── L2 hit → ~12 拍返回
├── L3 hit → ~40 拍返回
└── L3 miss → 等 DDR 内存 ~200+ 拍
⑥ 写回 — load 结果写入物理寄存器,ROB 标记完成3.2 Store 路径
① 发射 — 保留站把 store µop 发射到 LSU
② AGU — 计算目标地址
③ 写入 — 地址 + 数据写入 store buffer
④ 释放 — store µop 从流水线释放(不等 D-Cache 写完成)
⑤ 退休 — store 指令在 ROB 中退休
⑥ 刷入 — LSU 把 store buffer 中已退休的条目异步写入 L1 D-Cache
如果该 cache line 不在 L1 → 先做 RFO(Read-For-Ownership)关键洞察:store 在步骤③ 写 store buffer 后就走完了"执行",不阻塞流水线。真正的"写内存"(步骤⑥)是异步的,在下游慢慢做。这意味着 store 的"完成"比"对外可见"早——这是多核编程中 store buffer 引入内存重排的根源(详见 memory-reordering)。
3.3 Store→Load Forwarding:隐藏延迟的关键机制
mov [rsi], rax ; store 到 [rsi]
mov rbx, [rsi] ; load 同一地址 [rsi]按流程,load 要等 store 刷入 D-Cache 才能读到新值,可能等几十拍。但 LSU 做了聪明的事:load 发地址后,先搜 store buffer,发现 [rsi] 正好有一条未退休的 store,直接拿 store buffer 里的值给 load——零额外延迟。
硬件逐位比较 load 地址与 store buffer 中每条 store 的地址,判定 forwarding:
- 完全匹配(地址完全相同)→ 直接转发
- 部分重叠(store 写 8 字节,load 读前 4 字节在同一地址)→ 部分转发,或保守回退等待 store 完成
- 不匹配 → 正常走 D-Cache
forwarding 失败(比如地址不对齐、部分重叠无法合并)的惩罚很重——load 必须等 store 真实写入 D-Cache 后才能读,这意味着 ~10 拍以上的空等。
3.4 Memory Disambiguation:load 能不能超越前面的 store
乱序执行中,调度器可能把程序序上排在后面的 load 提前发到 LSU。这时需要判断:这条 load 的地址会不会和前面还没执行完的 store 冲突?
- 地址不同(disambiguated)→ load 可以安全超越 store,提前执行。
- 地址相同(aliased)→ load 必须等 store,否则会读到旧值(store→load ordering 违反)。
- 地址未知(store 的 AGU 还没算完)→ 保守等待,或者投机执行(若后来发现冲突则 flush 流水线 + 重放 load)。
现代 CPU 有专门的 memory disambiguation predictor(存储歧义消除预测器),记录历史上哪些 load-store 对发生过冲突,指导投机决策。预测错误则触发 pipeline flush(~10~20 拍惩罚)。
四、限制与瓶颈
| 限制 | 说明 | 表现 |
|---|---|---|
| AGU 端口数量 | 最多 3~4 个 AGU,密集的连续 load/store 请求填满 AGU 后,下游再多 ALU 也闲着 | 纯访存代码(memcpy、streaming)的 IPC 上限 = AGU 数量 |
| store→load forwarding 失败 | 地址不对齐、部分重叠、store buffer 不命中时,load 必须等 store 写回 D-Cache | ~10+ 拍延迟,常见于结构体成员混用、强转指针的场景 |
| Store Buffer 满 | store buffer 满了,新的 store 不能发射,前端停摆 | resource_stalls.sb 高,触发于 store 密集的代码 |
| D-Cache miss | L1 不命中,等 L2/L3/DDR | 几十到几百拍延迟,store buffer 里的 store 排不出去 |
| Memory disambiguation 误预测 | 预测器误判 load 和 store 不冲突,投机 load 读到旧值 → flush | pipeline flush + 重放,惩罚 10~20 拍 |
| 未对齐访存跨 cache line | 一次 load 跨两个 cache line 边界,LSU 拆成两次访存 | 两倍 AGU 占用 + 两倍延迟(详见 memory-alignment) |
LSU 是超标量宽机器的瓶颈点——不管你有 8 个 ALU + 4 个 FMA + 4 个 SIMD ALU 共 18 个执行单元,只要代码是密集访存的(
-O0产生的 spill 代码、链表遍历、哈希表查找),AGU 的 3~4 个端口就把 IPC 锁死在 3~4 以下。这也是为什么编译器优化的第一步是把变量留在寄存器、减少访存。
五、和周边部件的关系
保留站 ──→ 执行单元(ALU/FMA/SIMD) ──→ ROB
│
└──→ LSU ──→ L1 D-Cache ──→ L2 ──→ L3 ──→ DDR
│
└── store buffer (暂存未退休store,异步写入D-Cache)- 保留站:load/store µop 从保留站发射到 LSU 的 AGU。详见 reservation-station。
- ROB:store 指令在 ROB 退休后,其 store buffer 条目才被 LSU 刷入 D-Cache。详见 rob。
- Store Buffer:多核内存序的关键参与者——store buffer 中的值对其他核不可见,这是 StoreLoad 重排的根源。详见 store-buffer。
- Cache 子系统:LSU 是访问 D-Cache 的唯一入口。详见 cache-organization。
- TLB:AGU 算出地址后,TLB 负责 VA→PA 翻译。详见 tlb。
- 数据冒险:data-hazards——load→use RAW 是 LSU 路径上最严重的冒险类型,L1 miss 场景下依赖链可长达 14+ 拍。
- 纵切面走读:uop-pipeline-walkthrough——Store Buffer / Load Buffer 槽位生命周期序列图、SB/LB/LSU 三者层级关系。
六、观测:LSU 相关的 perf 事件
perf stat -e l1d_pend_miss.pending,l2_rqsts.all_demand_data_rd,resource_stalls.sb ./app
# l1d_pend_miss.pending — L1 D-Cache miss 未完成数(越高说明访存越密集)
# l2_rqsts.all_demand_data_rd — L2 接收的 load 请求总数
# resource_stalls.sb — 因 store buffer 满导致的前端停摆周期数resource_stalls.sb高 → store 生成速度超过 LSU 刷入 D-Cache 的速度,优化方向:检查是否有大量小粒度 store(可用 wider store 替代)、或使用 NT store(non-temporal,绕开 cache 直接写内存)。l1d_pend_miss.pending持续高位 → L1 D-Cache 是瓶颈,优化方向:改善数据布局(SoA 转 AoS、减少稀疏访存)、或加 prefetch 提前拉数据。
七、一句话总结
LSU 是超标量核里唯一的访存出口——AGU 数量(3~4 个)决定了每拍最多做多少次访存,这是密集访存代码 IPC 的硬上限。store→load forwarding 消除了同地址 store-load 对的延迟,但 forwarding 失败(不对齐、部分重叠)的惩罚很重。LSU 填满后,再多 ALU/FMA 也空转——编译器优化的方向就是把变量留在寄存器、别动内存。