µop 的类型与切分 · 关键机制:x86 指令怎么变成微操作
这是 uop-pipeline-walkthrough(µop 级流水线走读总纲)的拆分篇之一,前置知识合一篇:µop 是什么、分几类、怎么切,加上 µop 走过哪些阶段、有哪些贯穿全文的机制。配套拆分篇:三条指令逐拍走读 · 三案对比 · buffer 生命周期。
µop 的类型(功能分类)
x86 译码器产生的 µop 按功能可以分成六大类。每一类从译码出来后,进入不同 RS(保留站)、走不同执行端口、经历不同的流水线路径。
六大类 µop
| 类别 | 典型来源(x86 指令) | 做什么 | 进哪个 RS | 执行端口(Skylake) | EX 延迟 |
|---|---|---|---|---|---|
| ALU | add, sub, and, or, xor, shl, shr, cmp, test, lea, inc, dec, mov reg,reg | 整数算术/逻辑/移位/比较,计算寄存器值 | INT RS(Unified RS) | Port 0 / 1 / 5 / 6 | 1 拍 |
| Load (LD) | mov eax, [rbx] 的 load 部分,add eax, [rsi] 的 load µop | AGU 算地址 → LSU 读 D-Cache → 数据写回物理寄存器 | INT RS | Port 2 / 3(AGU) | 4~5 拍(L1 hit) |
| Store Address (STA) | mov [rbx], eax 的地址 µop | AGU 算存储地址 → 写入 Store Buffer 地址域 | INT RS | Port 2 / 3 / 7(AGU) | 1 拍(只算地址) |
| Store Data (STD) | mov [rbx], eax 的数据 µop | 从物理寄存器读数据 → 写入 Store Buffer 数据域 | INT RS | Port 4(Store Data) | 1 拍(只搬数据) |
| FP / SIMD | addsd, mulsd, addps, mulps, fmadd132sd, cvtsi2sd(标量/向量浮点/SIMD 运算) | 浮点/向量运算(加减乘除、FMA、类型转换) | FP RS(独立调度域) | Port 0 / 1(FMA)/ Port 5(shuffle) | 3~5 拍(FMA 4~5 拍) |
| Branch (BR) | jmp, je, jne, call, ret | 计算分支方向→验证预测→误预测时触发流水线冲刷 | INT RS | Port 0 / 6(BRU) | 1 拍 |
为什么 FP µop 专有一个 RS:FP 执行单元的延迟远长于 ALU(FMA 4~5 拍 vs ALU 1 拍)。如果把 FP µop 和 ALU µop 塞进同一个 RS,慢速 FP µop 会挤占 RS 槽位——长延迟意味着它占槽位久,影响 RS 利用率。独立 FP RS 让 FP 和整数两条调度通道完全解耦,互不阻塞。Skylake 的 INT RS = 97 条目,FP RS = 64 条目。
为什么 store 要拆成 STA + STD:Store 有两个独立操作数——地址和要写的数据。在保留站里,地址可能先就绪而数据还得等(比如地址寄存器 rbx 已就绪,但数据寄存器 eax 还被前一条长延迟加法占用)。拆开以后,STA 可以先发射(算好地址送入 Store Buffer),STD 等数据就绪再发射——这不影响 STA 已做完的工作。如果把它们绑成一条 µop,就得等两个操作数都齐了才能发射。
从 µop 类型看执行端口分配(Skylake 核心图)

同一拍的 µop 可以同时发射到不同端口,但每个端口一拍只能发一条。
n条同类型 ALU µop 即使操作数全齐,也只能每拍发射 4 条(最多 4 个 ALU 端口),这就是端口冲突——乱序执行也无法绕过的结构瓶颈。
特殊 µop —— macro-op fusion
某些 x86 指令对在译码阶段会融合成一条 µop,不算两条:
| x86 指令对 | 融合后的 µop | 说明 |
|---|---|---|
cmp + jcc | 1 条 CMP+BR µop | 比较+条件跳转融成一条,同时做比较和分支判断 |
dec + jnz | 1 条 DEC+BR µop | 减一+非零跳转(循环计数器结尾) |
add/sub + jcc | 1 条 ALU+BR µop | 加减后立即条件跳转 |
这不是两件事"合并执行"——是译码器识别出这对指令后,不拆成两条独立的 ALU+BR µop,而是在 µop cache(DSB)里存成一条融合 µop。省了一条 µop = 省了一个 RS 槽位 + 省了一个发射带宽。
µop 类型与流水线路径的关系
不同 µop 类型走出完全不同的流水线路径:
| µop 类型 | 路径 | 关键差异 |
|---|---|---|
| ALU | RS → ALU Port → 结果直接写回 PRF → WB | 最短路径,1 拍出结果,EX 拍就可用(前推到下一拍) |
| LD | RS → AGU → TLB → L1 D-Cache → 等 4~5 拍 → 数据写回 PRF → WB | EX 拍只算地址,真正读缓存发生在 MEM 级,数据到 WB 才进 PRF |
| STA | RS → AGU → 地址写 Store Buffer | EX 拍算地址 → Store Buffer 存入即完成,不等 D-Cache 写入 |
| STD | RS → Store Data Port → 数据搬入 Store Buffer | EX 拍搬数据到 Store Buffer 对应槽位 |
| FP | FP RS → FMA Port → 3~5 拍浮点流水线 → 写 FP PRF → WB | 专用 FP RS + 专用 FP 物理寄存器堆,与整数完全隔离 |
| BR | RS → BRU Port → 算方向 → 和 BPU 预测比对 → 通过则继续 / 不通过则冲刷 | 分支执行发生在 EX 拍,但误预测的冲刷代价是多拍流水线 = 等待被冲刷 µop 从后端所有部件清空 |
PR(Physical Register,物理寄存器)和 PRF(Physical Register File,物理寄存器文件):
架构寄存器(RAX/RBX/XMM0 等)只有 16 个(整数)+ 16 个(浮点),但 CPU 内部实际的寄存器远不止这些。PRF 就是这些物理寄存器的存储阵列(Skylake 整数 PRF = 180 条目,FP PRF = 168 条目),里面每一个条目就是一个 PR。
重命名之后,所有 µop 读写的都是 PR(P15, P28, P45...),不再是架构寄存器号。"P" 开头的编号(P15/P28/P45/FP_PR P12)就是物理寄存器号。PRF 比架构寄存器多出来的那些 PR 存的是"飞行中"的中间结果——退休时架构寄存器才指向最新提交的 PR,多出来的 PR 还在等消费者读完才归还。
整数 PRF 和 FP PRF 是两块独立的物理存储——这也是为什么 FP µop 有专用 FP RS + 专用 FP PRF(编号写作 FP_PR Px),跟整数 µop 从存储到调度都彻底隔离。
理解了这些 µop 类型,后面的案例就走得通:看到
mov [rbx],eax拆成 STA+STD,就知道它把地址和数据拆给了不同执行端口,Store Buffer 做中间聚合;看到add eax,[rsi+8]拆成 LD+ADD,就知道 LD µop 走 LSU→L1 D-Cache,ADD µop 走 ALU,两者在保留站里有 RAW 依赖链。
x86 指令 → µop 的切分
x86 是 CISC 指令集,一条指令可能做多件事。现代 CPU 的前端译码器(MITE/DSB/LSD)把 x86 指令切分成 µop——每条 µop 做一件事:
指令 → µop 分解 说明
mov [rbx], eax → STA [rbx] (Store Address) µop1: 计算目标地址 rbx → Store Buffer
STD eax (Store Data) µop2: 把 eax 值搬入 Store Buffer 数据域
add eax, [rsi+8] → LD [rsi+8] → temp (Load) µop1: 算地址、读 D-Cache → 临时值
ADD eax, temp (ALU) µop2: 加法(等 µop1 的结果)
addsd xmm0, [rdi+rcx*8] → LD [rdi+rcx*8] → t (Load) µop1: 算地址(索引*8 + 基址)、读 D-Cache
ADDSD xmm0, t (FP) µop2: 标量浮点加法(进 FP RS)| 指令 | 拆分 µop 数 | 拆分后类型 |
|---|---|---|
mov [rbx], eax(纯 store) | 2 | STA + STD |
add eax, [rsi+8](load+op) | 2 | LD + ALU |
addsd xmm0, [rdi+rcx*8](load+FP op) | 2 | LD + FP |
add rax, rbx(纯寄存器) | 1 | ALU |
记住这个规律:一条 x86 指令 = 1 个"运算"µop + 最多 1 个"访存"µop(按需)。纯寄存器指令只有 1 条 µop;带内存操作数的指令拆成 2 条;store 因为地址和数据是两个独立操作数,拆成 STA+STD 两条(这是唯一的特例,拆出 2 条"访存"µop)。译码器一次(每周期)最多译 4 条 µop(MITE),µop cache(DSB)每拍喂 6 条 µop——所以µop 条数直接决定前端吞吐,指令拆得越少越好。
µop 在流水线中走过的全部阶段
从 µop 的视角看,一条 µop 从"出生"(译码)到"死亡"(退休)要穿过七个阶段:

| 阶段 | 缩写 | 核心动作 | 耗时 |
|---|---|---|---|
| 取指 | IF | 从 I-Cache 取 x86 指令字节,送译码器 | 1 拍(I-Cache 命中) |
| 译码 | ID | x86 指令 → µop 序列,识别操作码、寄存器号 | 1 拍 |
| 重命名 | REN | 查 RAT 把架构寄存器号换成物理寄存器号;目的寄存器分配新 PR;ROB 分配槽位 | 1 拍 |
| 保留站 | RS | µop 在槽位里等源操作数就绪(tag 匹配);就绪后被调度器挑中发射 | 可变(依赖链长度) |
| 执行 | EX | ALU 算结果 / AGU 算地址 / LSU 读 D-Cache / 浮点单元算 FMA | 1-5 拍(D-Cache 命中 ~4-5 拍) |
| 写回 | WB | 结果写入目的物理寄存器;结果总线广播 tag → RS 里等待它的 µop 被唤醒 | 1 拍 |
| 退休 | RET | ROB head 的 µop 按序退休:store µop 标记 store buffer 可刷入 D-Cache;释放旧物理寄存器 | 1 拍(可批量 4~8 µop) |
后面逐拍走读时,每拍都标清楚每条 µop 处于哪个阶段、在做什么。
贯穿全文的关键机制(术语前置)
以下概念在三条案例里反复出现,这里先定义清楚,后面不再重复解释。
Store Buffer —— 为什么 store 不直接写 D-Cache
store 指令 → [ Store Buffer ] → (退休后) LSU 异步排空 → L1 D-Cache
▲
先写到这里就"完成"了
不用等 D-Cache 写入mov [rbx], eax 如果直接写 L1 D-Cache,需要先拿到 cache line 的独占权(MESI 协议),这期间流水线得 stall——几十到几百拍。Store Buffer 是 LSU 里的一个硬件队列(Skylake ~56 条目),store µop 只把地址和数据塞进去就算"执行完毕",退休后 LSU 再在后台异步地把它们刷入 D-Cache。
关键后果:本核后续的 load 如果读的正是一个"已在 Store Buffer 里但还没刷入 D-Cache"的地址,LSU 必须直接从 Store Buffer 把数据前推给 load,而不是去读 D-Cache(D-Cache 里还是旧值)。这叫 store-to-load forwarding。但其他核看不到你的 Store Buffer——这就是 StoreLoad 重排的根源。
RAT + 结果总线 + tag 广播 + 唤醒
这是寄存器重命名之后,依赖 µop 之间通信的完整链路:
生产者 µop 在 REN 阶段分配目的 PR(如 P45)
→ 消费者 µop 在 REN 阶段查 RAT,发现源操作数映射到同一个 PR(P45),tag 标记"未就绪"
→ 消费者在 RS 里等待,监听结果总线
→ 生产者执行完毕,结果总线广播 "P45 就绪!值 = 0x05"
→ RS 里的消费者 tag 匹配 → 源操作数标记就绪 → 被调度器挑中发射| 概念 | 是什么 | 在哪 |
|---|---|---|
| PR | 物理寄存器,PRF 存储阵列里的一个条目(P15/P28/P45 或 FP_PR P12),每个存一个值 | RS→EX→WB |
| PRF | 所有 PR 组成的存储阵列,整数 180 条目、FP 168 条目 | EX 读、WB 写 |
| RAT | 架构寄存器 → 物理寄存器号的映射表 | REN 阶段查/写 |
| PR tag | 每条 µop 的源/目的物理寄存器编号,源 tag=0 表示值已就绪 | RS→WB |
| 结果总线 | 执行单元写回 PRF 时广播 tag 的总线,所有 RS 槽位都在监听 | WB 阶段 |
| 唤醒 | RS 里的等待 µop 监听到源 tag 匹配 → 标记就绪 | RS 阶段 |
| 前推网络 | 执行单元输出直接旁路到下游执行单元输入,省 1 拍 PRF 读写延迟 | EX 拍内 |
为什么需要 tag 广播:重命名之后 µop 只知道"我等的是 P45",不知道 P45 什么时候产生。结果总线广播就是硬件版的"P45 好了!"——所有 RS 槽位同时监听、同时匹配,匹配上的那个 µop 立即被唤醒。
Store Buffer 的"可提交"与"排空"
| 阶段 | 触发条件 | 能去哪 |
|---|---|---|
| 填充 | STA µop 执行(写地址域)+ STD µop 执行(写数据域) | 槽位里地址+数据都有了 |
| 标记可提交 | STA 和 STD 在 ROB 中都退休了 | Store Buffer 槽位状态变更为 eligible |
| 排空 (drain) | LSU 找机会(有空闲 D-Cache 写端口、该地址 cache line 处于可写状态) | 槽位数据刷入 L1 D-Cache,槽位归还 |
| store forwarding | 同核 load 地址匹配到已填充但未排空的槽位 | 数据从 Store Buffer 直送给 load,不走 D-Cache |
排空没有单独的 µop,不是流水线的一部分。它发生在 µop 退休之后,由 LSU 硬件状态机自主完成。
前置知识齐了,现在可以逐拍走读三条指令——从最"纯"的 store 指令开始:三条指令逐拍走读。