三案对比:时间线叠放与 µop 生命周期总表
这是 uop-pipeline-walkthrough(µop 级流水线走读总纲)的拆分篇之一,把三条案例指令的时间线叠在一起看规律。配套拆分篇:µop 类型与切分 · 关键机制 · 三条指令逐拍走读 · buffer 生命周期。
把三条指令的全部 µop 从取指到退休的流水线时间线叠在一起对比。
案例一:mov [rbx], eax(~6 拍,无 RAW 依赖)
STA 和 STD 两个操作数相互独立,同拍进入 RS 后可以同一拍或相邻拍发射。
| µop | 拍1 | 拍2 | 拍3 | 拍4 | 拍5 | 拍6 |
|---|---|---|---|---|---|---|
| STA | IF | ID | REN | RS | EX | RET |
| STD | — | IF | ID | REN | EX | RET |
STA 的 EX 走 Port 2 AGU(算地址写 Store Buffer 地址域),STD 的 EX 走 Port 4 Store Data(搬数据进 Store Buffer 数据域)——互不争端口,可同拍发射。退休后 LSU 异步排空到 L1 D-Cache。
案例二:add eax, [rsi+8](~7 拍,RAW 依赖,load→use = 2 拍)
| µop | 拍1 | 拍2 | 拍3 | 拍4 | 拍5 | 拍6 | 拍7 |
|---|---|---|---|---|---|---|---|
| LD | IF | ID | REN | RS | LSU | WB | RET |
| ADD | — | IF | ID | REN | ⏸ RS | EX | RET |
⏸ = 等待,◀ = 前推旁路
ADD µop 在拍 2 REN 阶段就通过 RAT 拿到了 P45 tag,但必须等 LD µop 的 LSU 完成(拍 4-5)、结果在拍 5 写回 PRF 并广播 tag 后才被唤醒。唤醒后 ALU 通过前推网络直接取 P45 的值(跳过一次 PRF 读),一拍算出结果。
案例三:addsd xmm0, [rdi+rcx*8](~14 拍,跨集群 + FP 4 拍流水线)
| µop | 拍1 | 拍2 | 拍3 | 拍4 | 拍5..8 | 拍9 | 拍10..13 | 拍14 |
|---|---|---|---|---|---|---|---|---|
| LD | IF | ID | REN | RS | LSU×4 | WB | RET | |
| ADDSD | — | IF | ID | REN | ⏸ FP | ⏸ | FMA×4 | RET |
拍 10..13 的 FMA×4 在专有 FP 执行流水线内完成,与整数 RS 完全隔离。
ADDSD µop 的关键瓶颈:
- LD µop 走整数 RS + 整数 PRF,结果 fp_tmp 写回 FP PRF(需要跨集群搬运,额外 1 拍)
- ADDSD µop 走浮点 RS + FP PRF,且 FP FMA 单元自身 4 拍流水线
- 因此从 IF 到 RET 总延迟 ~14 拍,其中等待 + FP 流水线占 ~9 拍
三案总对比
| 维度 | mov [rbx], eax | add eax, [rsi+8] | addsd xmm0, [rdi+rcx*8] |
|---|---|---|---|
| µop 数 | 2(STA + STD) | 2(LD + ADD) | 2(LD + ADDSD) |
| µop 间依赖 | 无 RAW | 有 RAW(µop₂ 等 µop₁) | 有 RAW(µop₂ 等 µop₁) |
| µop 在同一个 RS? | 是(INT RS) | 是(INT RS) | 否(INT RS + FP RS) |
| 最短总延迟(L1 命中) | ~6 拍 | ~7 拍 | ~14 拍 |
| 延迟瓶颈 | 无(可并行发射) | load→use 2 拍 | FP 流水线 4 拍 + 跨集群 |
| 退休时做什么 | 标记 store buffer 可提交 | 释放 eax 旧 PR | 释放 xmm0 旧 FP_PR |
规律一:指令本身的运算量决定了 µop 数,三条都是 2 µop,但延迟从 6→7→14 拍递增,差距全在"等待"——store 无等待,load 等 2 拍(L1 命中),FP 要等 load + 跨集群 + 4 拍 FMA 流水线。
规律二:总延迟 ≈ 关键路径上各执行单元延迟之和,和"指令条数"无关。优化一条依赖链的关键是缩短关键路径上的每一拍(缓存命中、前推、合并同类运算)。
规律三:store 把最长的工作移出了关键路径——写进 Store Buffer 就算完成,D-Cache 更新是退休后的后台异步行为;load 慢在必须真的读 D-Cache(4~5 拍);FP 最慢在 FMA 自身 4 拍流水线 + 跨集群通信。延迟高低排序:store < load < FP。
延迟拆解:三条指令各拍花在哪
| 阶段 | store | load+ALU | FP load+FMA |
|---|---|---|---|
| IF→ID→REN(固定 3 拍) | 3 | 3 | 3 |
| RS 等待(可变) | 1 | 2(等 LD 数据) | 3(等 LD + 跨集群) |
| EX 执行 | 1(并行) | LD 4~5 + ALU 1 | LD 4~5 + FMA 4 |
| WB→RET(固定 2 拍) | 2 | 2 | 2 |
| 合计 | ~6 | ~7 | ~14 |
三列的"固定成本"(IF→ID→REN + WB→RET)都是 5 拍,差异完全来自可变部分:
- store 的 RS 等待接近 0——两条 µop 无依赖,进站即发
- load+ALU 的 RS 等待 2 拍——ADD 必须等 LD 的结果写回并广播
- FP 的 RS 等待 3 拍——比 load+ALU 多出的 1 拍是跨集群广播走线;EX 又多出 3 拍(4 拍 FMA − 1 拍 ALU)
注意 RS 等待不是"空闲":µop 在等数据期间不占发射带宽,调度器会把同周期内其他已就绪的 µop 填进空出来的发射槽——这正是乱序执行掩藏延迟的手段,也是对比三案时最容易被忽略的一列。
为什么 store 总是最快、FP 总是最慢
store 快不是因为它"简单",而是把最长的那段工作(写 D-Cache)移出了关键路径——写进 Store Buffer 就算完成,剩下的由 LSU 后台异步处理,不占流水线。load 慢在必须真的去读 D-Cache 而且结果要被等;FP 最慢是因为 FMA 单元自身就是 4 拍流水线,比 ALU 的 1 拍长一个量级,还叠了跨集群通信。
这条规律对写高性能代码有直接含义:热点循环的瓶颈排序通常是 load > FP > store——优先优化访存,其次优化浮点依赖链,store 一般不用管(除非 Store Buffer 写满阻塞)。
从前端到退休:µop 在各部件的生命周期总表
把三组 µop 的轨迹汇总成一张通用生命周期表:
| 阶段 | 部件 | µop 做了什么 | 输入 | 输出 | 耗时 |
|---|---|---|---|---|---|
| IF | 取指单元 | 从 I-Cache 取 x86 指令字节,PC 指向下一条 | PC 值 | 指令字节流(16/32 字节/拍) | 1 拍 |
| ID | 译码器 | x86 指令 → µop 序列,识别操作码、寄存器号、立即数 | 指令字节 | µop 序列(含类型、架构寄存器号、立即数) | 1 拍 |
| REN | 重命名引擎 | 架构寄存器号 → 物理寄存器号(查 RAT);目的寄存器分配新 PR(从空闲列表);ROB 分配槽位;store 指令分配 store buffer 槽位 | 架构寄存器号 | µop 携带物理寄存器 tag、ROB 槽位号 | 1 拍 |
| RS | 保留站 | µop 在槽位里等源操作数就绪(tag 匹配);就绪后被调度器挑中发射 | 源 tag | 发射到执行端口 | 可变 |
| EX | 执行单元 | ALU 算结果 / AGU 算地址 / LSU 查 D-Cache / FMA 算浮点 / STA 写 store buffer 地址 / STD 写 store buffer 数据 | 操作数值、地址 | 结果值、store buffer 条目 | 1~5 拍 |
| WB | 结果总线 | 结果写入目的物理寄存器;广播 tag 唤醒 RS 中的依赖 µop | 结果值 + 目的 PR tag | RS 中依赖 µop 的源 tag 清零 | 1 拍 |
| RET | ROB | 按序退休:store µop 退休后 store buffer 槽位标记可提交;释放旧 PR 归还空闲列表 | ROB head 指针 | 架构状态更新、PR 回收 | 1 拍 |
这张表里耗时列最有价值的是 RS 那行的"可变":七级阶段里六级的耗时是固定的(1 拍或 4~5 拍),唯独 RS 等待由程序的数据依赖决定。µop 生命周期里真正"可以被软件优化"的部分,几乎全部集中在这一级——缓存命中率、指令级并行度、分支预测质量,最终都反映为 RS 等待时间的长短。
从三案对比到实际调优
走读完三条指令,回头做性能分析时,"拍数"就有了参照系:
- 一段热点循环如果每轮要 ~14 拍(像案例三的 FP 依赖链),3 GHz 下就是每秒约 2.1 亿轮——想再快就得拆依赖链或向量化。
- perf 里的 CPI(每指令周期数)除以"关键路径拍数/µop 数",可以粗略估算依赖链的暴露程度:CPI 明显高于理论值,说明 RS 等待占了主导。
- 编译器选项(-O2/-O3)和手工优化(内存布局、减少跨集群转换)改变的主要是 RS 等待和 EX 延迟;IF→ID→REN 的 3 拍固定成本省不掉。
三条指令走完,还差最后一块拼图:store buffer / load buffer 这两个队列型部件在指令生命周期之外如何运转——见 buffer 生命周期。