﻿# LSU（Load/Store Unit）—— 访存流水线的命门

> 承 [pipelining-superscalar.md](/concepts/microarch/pipelining-superscalar.md) §三.3.4 的 LSU 初步介绍和 [store-buffer.md](/concepts/memory-ordering/store-buffer.md) 的 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，把访存路径从运算流水线中抽出来。

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<fe>>  #BBDEFB
  BorderColor<<fe>>      #1565C0
  BackgroundColor<<lsu>> #C8E6C9
  BorderColor<<lsu>>     #2E7D32
  BackgroundColor<<dc>>  #FFE0B2
  BorderColor<<dc>>      #E65100
}
rectangle "INT/FP 调度器\n(发射到执行单元)" <<fe>> as SCHED
rectangle "══════════ LSU ══════════\n┌──────────────────────────────┐\n│  AGU × 3~4                   │\n│  地址生成:base+index*scale+off│\n│──────────────────────────────│\n│  Store Buffer (几十个条目)     │\n│  store先写这里,退休后刷D-Cache │\n│──────────────────────────────│\n│  Load Buffer (几十个条目)      │\n│  跟踪飞行中的load请求          │\n│──────────────────────────────│\n│  Store→Load Forwarding         │\n│  同一地址刚store又load,旁路直给 │\n└──────────────────────────────┘" <<lsu>> as LSU
rectangle "L1 D-Cache\n32~48KB\n4~5 拍延迟" <<dc>> as L1D
SCHED -down-> LSU : load/store µop\n进入 LSU 队列
LSU -down-> L1D : 地址翻译后\n访问 D-Cache
@enduml
```

## 二、功能拆解

| 部件 | 做什么 | 并行度/容量 |
|------|--------|-----------|
| **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.md](/concepts/memory-ordering/store-buffer.md)。本篇聚焦 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` 等），其目标地址由 **四段拼成**：

```bash
有效地址 = 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） |

分开有两个原因：

1. **端口隔离**：如果把地址计算和 ALU 运算混在一起分到同一个端口上，一条长延迟指令（如 DIV）会把跟它无关的 load/store 也堵住——即使 load 的基址寄存器早就就绪了。AGU 有独立端口后，load/store 只跟其他 load/store 抢 AGU，不和 ALU 抢。
2. **流水线分叉**：ALU 的结果下一拍进 MEM 级透传就完了；AGU 算完地址后要走**完全不同的路径**——TLB 翻译 VA→PA、store buffer 前向查找、D-Cache Tag 比较、数据读取 / 写入——这些物理上都在 LSU 和 D-Cache 流水线里，跟 ALU→PRF 写回路径不走同一套硬件。

#### AGU 端口分配（以 Skylake 客户端为例）

```bash
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 链中的位置

```bash
保留站(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 路径

```bash
① 发射  — 保留站把 load µop 发射到 LSU
② AGU   — 计算访存地址（如 rax+rsi*8+0x10）
③ 翻译  — TLB 把 VA 翻译成 PA（TLB miss → page walk，[tlb.md](/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 路径

```bash
① 发射  — 保留站把 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.md](/concepts/memory-ordering/memory-reordering.md)）。

### 3.3 Store→Load Forwarding：隐藏延迟的关键机制

```asm
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.md](/concepts/cache/memory-alignment.md)）|

> **LSU 是超标量宽机器的瓶颈点**——不管你有 8 个 ALU + 4 个 FMA + 4 个 SIMD ALU 共 18 个执行单元，只要代码是密集访存的（`-O0` 产生的 spill 代码、链表遍历、哈希表查找），AGU 的 3~4 个端口就把 IPC 锁死在 3~4 以下。这也是为什么编译器优化的第一步是把变量留在寄存器、减少访存。

## 五、和周边部件的关系

```bash
保留站 ──→ 执行单元(ALU/FMA/SIMD) ──→ ROB
                │
                └──→ LSU ──→ L1 D-Cache ──→ L2 ──→ L3 ──→ DDR
                       │
                       └── store buffer (暂存未退休store，异步写入D-Cache)
```

- **保留站**：load/store µop 从保留站发射到 LSU 的 AGU。详见 [reservation-station.md](/concepts/microarch/reservation-station.md)。
- **ROB**：store 指令在 ROB 退休后，其 store buffer 条目才被 LSU 刷入 D-Cache。详见 [rob.md](/concepts/microarch/rob.md)。
- **Store Buffer**：多核内存序的关键参与者——store buffer 中的值对其他核不可见，这是 StoreLoad 重排的根源。详见 [store-buffer.md](/concepts/memory-ordering/store-buffer.md)。
- **Cache 子系统**：LSU 是访问 D-Cache 的唯一入口。详见 [cache-organization.md](/concepts/cache/cache-organization.md)。
- **TLB**：AGU 算出地址后，TLB 负责 VA→PA 翻译。详见 [tlb.md](/concepts/cache/tlb.md)。
- **数据冒险**：[data-hazards.md](/concepts/microarch/data-hazards.md)——load→use RAW 是 LSU 路径上最严重的冒险类型，L1 miss 场景下依赖链可长达 14+ 拍。
- **纵切面走读**：[uop-pipeline-walkthrough.md](/concepts/microarch/uop-pipeline-walkthrough.md)——Store Buffer / Load Buffer 槽位生命周期序列图、SB/LB/LSU 三者层级关系。

## 六、观测：LSU 相关的 perf 事件

```bash
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 也空转——编译器优化的方向就是把变量留在寄存器、别动内存。**

