﻿# CISC 与 RISC —— 现代 x86 的双面人生

> 你写的 `add eax, [rsi+8]` 是一条 CISC 指令，但 CPU 内部跑的是一串 RISC 风格的 µop。**x86 是 CISC 的壳、RISC 的芯**——这个"壳"止于译码器。理解这条分界线，是看懂 µop 级流水线、perf 前端计数器、以及为什么"指令条数 ≠ µop 数"的前提。

## 一、CISC 与 RISC 的哲学分歧

这场架构之争可以追溯到 20 世纪 80 年代。两种设计哲学的出发点完全不同：

```plantuml
@startuml
title CISC vs RISC：设计哲学对比

rectangle "**CISC（Complex Instruction Set）**\n代表：x86（Intel/AMD）" as CISC #FFF3E0 {
  rectangle "一条指令干多件事" as C1 #FFECB3
  rectangle "变长编码 1~15 字节\n操作数可以是内存地址" as C2 #FFECB3
  rectangle "寄存器少（x86-64: 16个 GPR）\n编译器压力小" as C3 #FFECB3
  rectangle "硬件复杂度高\n译码器最贵" as C4 #FFECB3
}

rectangle "**RISC（Reduced Instruction Set）**\n代表：ARM / RISC-V / MIPS" as RISC #E3F2FD {
  rectangle "一条指令只干一件事" as R1 #BBDEFB
  rectangle "定长编码（ARM 4字节）\n只有 Load/Store 访存" as R2 #BBDEFB
  rectangle "寄存器多（ARM: 31个 GPR）\n编译器压力大" as R3 #BBDEFB
  rectangle "硬件简单规整\n译码器几乎零开销" as R4 #BBDEFB
}

@enduml
```

| 维度 | CISC（x86） | RISC（ARM / RISC-V） |
|------|------------|---------------------|
| 指令长度 | 变长 1~15 字节 | 定长 4 字节（ARM）/ 2 或 4（RISC-V） |
| 访存方式 | 任意 ALU 指令可访存：`add eax, [rsi+8]` | Load/Store 架构：必须 `ldr` → `add` 分开 |
| 寻址模式 | 丰富：`[base + index*scale + offset]` 单指令完成 | 简单：`ldr x0, [x1, #8]` 只有 base+offset |
| 架构寄存器数 | 16 个 GPR + 16/32 个 XMM/YMM/ZMM | 31 个 GPR + 32 个 FPR |
| 操作数格式 | 两操作数：`add eax, ebx`（eax 既是源又是目的） | 三操作数：`add x0, x1, x2`（dest ≠ src） |
| 硬件成本 | 译码器复杂度极高（预译码 + 4 路并行译码） | 译码器极简，省下的晶体管给寄存器和执行单元 |

> **一句话总结**：CISC 把复杂性推给硬件指令集，希望一条指令做更多事；RISC 把复杂性推给编译器，让硬件保持简洁。两种哲学没有绝对优劣——但现代高性能 CPU 已经在内部趋同了。

## 二、现代 x86 的真实面貌：CISC 壳 + RISC 芯

从 Pentium Pro（1995）开始，Intel 就走上了这条路。今天所有 x86 高性能处理器（Intel Core / AMD Zen）全部沿用这个架构：

```plantuml
@startuml
title 译码器：CISC → RISC 的唯一边界

actor "程序员 / 编译器" as PROG
PROG -> [译码器]: add eax, [rsi+8]\n一条 CISC 指令，同时访存+算数

rectangle "**译码器（Decoder）**\n── CISC→µop 的唯一分界线 ──" as DEC #FFCDD2

[译码器] -> [µop]: LD tmp, [rsi+8]\n(Load µop，只负责访存)
[译码器] -> [µop]: ADD eax, tmp\n(ALU µop，只负责做加法)

note right of [µop] #C8E6C9
  **µop 的特征（全部 RISC 风格）：**
  ✓ 定长（内部编码固定宽度）
  ✓ 三操作数：dest = src1 OP src2
  ✓ 单功能：一条 µop 只做一件事
  ✓ 寄存器全部是物理寄存器号
  ✓ 源和目的完全分离（没有"累加器"概念）
end note

rectangle "**µop 之后的全部世界**\n── RISC 执行引擎 ──" as RISC_ENGINE #E8F5E9 {
  rectangle "重命名\nRAT" as REN #C8E6C9
  rectangle "保留站\nRS" as RS #C8E6C9
  rectangle "执行单元\nALU/AGU/FMA" as EXEC #C8E6C9
  rectangle "LSU\nLoad/Store" as LSU #C8E6C9
  rectangle "ROB\n退休" as ROB #C8E6C9
}

[µop] --> REN

@enduml
```

**译码器之前**：变长指令、复杂寻址、操作数既是源又是目的——全是 CISC 的特征。  
**译码器之后**：定长 µop、三操作数、寄存器全部分配物理编号、一条 µop 只干一件事——全是 RISC 的特征。

> **译码器是 CPU 内部唯一的 CISC 部件，之后的所有流水线阶段都不关心这些 µop 是从 x86 来的还是 ARM 来的。**

### 2.1 为什么现代 x86 一定这样设计

如果不做 µop 分解，直接在超标量乱序流水线上跑 CISC 指令，会遇到三个不可能绕过的死结：

**死结一：指令宽度不可控。** CISC 指令长度 1~15 字节不等，预译码都不知道下一条指令的起点在哪里，超标量每个周期取多条指令的效率极低。拆成定长 µop 后，所有后续阶段的调度、发射、退休都以固定粒度进行。

**死结二：操作数语义混乱。** `add eax, [rsi+8]` 在 x86 层面是"读内存→做加法→写回 eax"一条指令，但"读内存"和"做加法"走的是完全不同的硬件通路（LSU vs ALU）。不拆开就没法在保留站里分别调度这两步——二者执行延迟差 4~5 拍，混在一起是调度器的噩梦。

**死结三：寄存器重命名无法工作。** `add eax, ebx` 的语义是 `eax = eax + ebx`，eax 既是源又是目的。重命名必须把源和目的分配到不同的物理寄存器（否则消不掉 WAW/WAR），但如果指令格式本身不给三操作数空间，RAT 就得靠额外的临时寄存器来拆——等于在 RAT 里重新做了一遍译码。不如直接在译码器里拆好。

## 三、译码器：CISC→µop 的边界到底发生了什么

### 3.1 译码流程

```bash
x86 指令流（变长，1~15 字节）
  │
  ├─→ 预译码（Pre-decode）：标出每条指令的边界
  │
  ├─→ 指令队列（Instruction Queue）
  │
  ├─→ 复杂译码器（Complex Decoder）：处理多 µop 指令（1 条/周期）
  └─→ 简单译码器 × 3~4（Simple Decoder）：处理单 µop 指令（1 条/周期每个）
       │
       └─→ µop 队列（µop Queue）→ µop Cache（DSB）→ IDQ（Instruction Decode Queue）
```

现代 Intel（Skylake 起）译码器布局：
- **4~6 路并**行译码（1 复杂 + 3~5 简单）
- 单周期最多译出 4~6 条 µop（不是 4~6 条 x86 指令）
- 复杂指令（如 `rep movsb`、`div`）由 MS-ROM（微码序列器）生成 µop 流

### 3.2 典型指令的 µop 拆分

| x86 指令 | µop 数 | 拆分结果 | 说明 |
|----------|:-----:|---------|------|
| `mov eax, ebx` | 1 | `MOV Pn, Pm` | 纯寄存器搬移，单 µop，简单译码器处理 |
| `add eax, ebx` | 1 | `ADD Pn, Pm, Pk` | 纯 ALU，单 µop，简单译码器 |
| `add eax, [rsi+8]` | 2 | `LD tmp, [rsi+8]` + `ADD eax, tmp` | 访存+算数合一指令，拆成两条 µop |
| `mov [rbx], eax` | 2 | `STA [rbx]` + `STD eax` | Store 拆成地址 µop + 数据 µop |
| `addsd xmm0, [rdi+rcx*8]` | 2 | `LD tmp, [rdi+...]` + `ADDSD xmm0, tmp` | 浮点 Load-Op，走的还是 INT RS（LD）+ FP RS（ADDSD） |
| `rep movsb`（大块内存拷贝） | N | 微码流（几十条 µop） | 由 MS-ROM 按模式展开，不在普通译码器里处理 |

### 3.3 µop Cache（DSB）：跳过译码器

x86 的译码器功耗大、面积大，而且受限于 4~6 路——如果循环体 20 条 x86 指令、拆出来 35 条 µop，每次迭代译码都要花 6~9 个周期。

**DSB（Decoded Stream Buffer，即 µop Cache）** 就是解决方案：已经译好的 µop 直接缓存起来，循环时跳过译码阶段，每个周期最多往 IDQ 送 6 条 µop。

| 场景 | 译码路径 | 吞吐 |
|------|---------|------|
| 代码首次执行 | x86 → 预译码 → 译码器 → µop Queue → IDQ（同时写入 DSB） | ≤ 4~6 µop/周期 |
| 循环命中 DSB | DSB → IDQ（跳过译码器） | 6 µop/周期，译码器空闲 |
| 循环命中 LSD | LSD → IDQ（连 DSB 也跳过了） | 环路最短，零前段开销 |

> 对性能分析来说，**perf 的 `idq.dsb_uops` / `idq.mite_uops` 分别统计 DSB 和传统译码路径来的 µop 数**，前者高是好事（前端高效），后者高意味着译码器是瓶颈。

## 四、对 perf 分析的启示

### 4.1 指令数 ≠ µop 数

```bash
perf stat -e instructions,idq.mite_uops,idq.dsb_uops -- ./your_program
```

- `instructions` 是退休的 x86 指令数（程序员的视角）
- `idq.mite_uops + idq.dsb_uops` 才是硬件真正执行的 µop 数（CPU 的视角）
- 两者差值就是译码器拆分开销

典型场景：
- 指令全是 `mov reg, reg` / 简单 ALU → µop 数 ≈ 指令数
- 大量 `add eax, [mem]` / `push/pop` 等内存操作数指令 → µop 数可高达指令数的 1.5~2 倍
- `rep stosb` / `div` 等微码指令 → µop 数远超指令数（几十倍）

### 4.2 前端 bound = 译码器 bound？

perf 的 `frontend_bound` 可以进一步用 `idq.mite_uops` vs `idq.dsb_uops` 判断：

| 迹象 | 含义 | 解法 |
|------|------|------|
| `idq.mite_uops` 占比高，IPC 低 | 译码器吞吐不够，代码路径长且复杂指令多 | 减少 load-op 合并（改用单纯 load + 单纯 ALU）、减小编译后的代码体积 |
| `idq.dsb_uops` 占比高，IPC 还是低 | 前端 DSB 吞吐够，瓶颈在后端（执行单元/缓存） | 分析后端 stall 来源 |
| DSB miss 率高 | µop cache 被热代码反复踢出，代码体积过大 | 热路径做 inlining 权衡、用 likely/unlikely 把冷代码移到函数末尾 |

### 4.3 一条指令"便宜"还是"贵"——看 µop 数

| x86 指令 | µop 数 | 隐藏成本 | 性能评价 |
|----------|:-----:|---------|---------|
| `lea rax, [rdi+rsi*2+8]` | 1 | 单 µop，但走 AGU 算地址 | 很便宜——lea 是免费的算术单元 |
| `add eax, ebx` | 1 | 单 µop，ALU 一拍 | 最便宜 |
| `add eax, [rsi+8]` | 2 | LD + ADD，两条 µop 之间有 RAW | 中等——别被一条 x86 指令迷惑 |
| `push rax` | 2（隐式 `sub rsp,8` + `mov [rsp],rax`） | STA + STD，还隐式更新 rsp | 比看起来贵 |
| `pop rax` | 2 | LD + `add rsp,8` | 同上 |
| `div` | 几十 | 微码流，阻塞译码器 | 极贵——循环里能免则免 |

## 五、与其他文档的关系

| 文档 | 关系 |
|------|------|
| **[uop-pipeline-walkthrough.md](/concepts/microarch/uop-pipeline-walkthrough.md)** | 本文说了"µop 长什么样、怎么来的"，walkthrough 讲了"µop 在流水线里逐拍怎么跑" |
| **[pipelining-superscalar.md](/concepts/microarch/pipelining-superscalar.md)** | 流水线的基础假设就是定长 µop——如果跑的是变长 CISC 指令，五级流水线无从搭建 |
| **[data-hazards.md](/concepts/microarch/data-hazards.md)** | `add eax, [rsi+8]` 拆成 LD+ADD——这条 RAW 依赖在 x86 汇编层根本看不到，只在 µop 层显形 |
| **[register-renaming.md](/concepts/microarch/register-renaming.md)** | 三操作数 µop 是重命名的前提——`dest, src1, src2` 的分离格式让 RAT 可以直接给 dest 和 src1 分不同的 PR |
| **[cpu-microarch-overview.md](/concepts/microarch/cpu-microarch-overview.md)** | 演进链总纲：µop 架构是超标量乱序的底层前提 |
| **[i-cache.md](/concepts/cache/i-cache.md)** | I-Cache 存的是 CISC 指令，DSB 存的是译好的 µop——两者的 miss 都是前端 stall |

> **一句话总结**：x86 的"复杂"只在译码器之前存在——从 µop 开始，它和 ARM/RISC-V 跑的是同一套 RISC 理念的流水线。理解这个分界线，才能看懂为什么 `add eax, [rsi+8]` 比 `mov eax, [rsi+8]; add eax, ebx` 多了一条隐式的 µop 级 RAW 依赖，以及为什么 perf 的 µop 计数器比指令计数器更接近硬件真相。

