三条指令逐拍走读:store / load / FP 的全旅程
这是 uop-pipeline-walkthrough(µop 级流水线走读总纲)的拆分篇之一,逐拍走读三条有代表性的指令:纯 store(无依赖、双 µop 并行)、load + ALU(有 RAW 依赖、load→use 延迟)、浮点 load + FP(跨集群调度)。配套拆分篇:µop 类型与切分 · 关键机制 · 三案对比 · buffer 生命周期。
三条指令一览(全部假设 L1 D-Cache 命中、无分支误预测、前端无瓶颈):
| 案例 | 指令 | µop 切分 | 看点 |
|---|---|---|---|
| 一 | mov [rbx], eax | STA + STD | 无依赖,双 µop 并行发射,Store Buffer 聚合 |
| 二 | add eax, [rsi+8] | LD + ADD | RAW 依赖,load→use 最小 2 拍延迟 |
| 三 | addsd xmm0, [rdi+rcx*8] | LD + ADDSD | 跨集群:INT RS 与 FP RS 之间 tag 广播 +1 拍 |
案例一:mov [rbx], eax —— store 指令的全旅程
假设条件:
- rbx = 0x7fff1000(有效虚拟地址),eax = 0x2a(要写入的值)
- 指令之前 rbx、eax 都已就绪;ROB 里没有未退休的指令堵在 head;Store Buffer 有空闲槽位
- [rbx] 对应的 cache line 在 L1 D-Cache 中(M 态,可直接写)
µop 切分与依赖分析
mov [rbx], eax
├── µop₁: STA (Store Address) — AGU 算目标地址,写入 store buffer 地址域
└── µop₂: STD (Store Data) — 把 eax 的值写入 store buffer 数据域为什么 store 要拆成 STA + STD(不绑成一条):
mov [rbx], eax要干两件彼此独立的事——算出地址(用 rbx)和拿到数据(用 eax)。两个源寄存器不同、走的执行端口不同(AGU Port 2/3/7 vs Store Data Port 4)、各自操作数未必同时就绪。绑成一条就得等两个操作数都齐才能发射,浪费 AGU 的调度机会;拆开后地址先就绪就先发 STA,数据晚到就晚发 STD,互不阻塞。
STA 和 STD 之间没有 RAW / WAR / WAW 依赖:两个源完全不同,也不写物理寄存器(store 不产生寄存器结果)。唯一的"汇合点"是 Store Buffer 的同一个槽位——地址写地址域、数据写数据域,填充的是不同字段,没有先后覆写冲突。所以可以同拍发射、可以任意先后。
µop 级流水线序列图
下图把所有参与者拉成纵向生命线,µop 按拍(时钟周期)从 IF 流动到 RET。注意 两条 µop 在不同执行端口并行展开——STA 走 Port 2 的 AGU,STD 走 Port 4 的 Store Data 端口,两者同拍发射、同拍执行。
![mov [rbx], eax (89 03) —— store 指令 6 拍全旅程 mov [rbx], eax (89 03) —— store 指令 6 拍全旅程](/plantuml/f2f2ae73222d29b162b6dc48a675b498ff58d55a.png)
各阶段的关键细节
拍 0-1(IF→ID):mov [rbx], eax 的 x86 编码为 89 03(2 字节,ModRM 编码)。译码器解析 opcode 89 = MOV r/m32, r32,ModRM 解码得到 reg=eax dst=[rbx],生成 STA + STD 两条 µop。
拍 2(REN):查 RAT 把 rbx→P15、eax→P28。STA 和 STD 都不写目的寄存器,不分配新 PR。ROB 各分配一个槽位(#42、#43),Store Buffer 分配同一个槽位 #7(STA 绑地址域,STD 绑数据域)。
注意:STA 和 STD 虽是同一条指令切出来的,但在 ROB 里各占一个槽位。Store Buffer 槽位 #7 在 µop₁ 退休前就已写入地址,但只有等 µop₂ 也退休后,槽位才标记"可提交"。
拍 3(RS):两条 µop 进 INT RS,源 tag(P15、P28)早已就绪,调度器看到 Port 2 AGU 和 Port 4 STD 都空闲,同拍发射。
拍 4(EX):µop₁ 的 AGU 算 VA = 0x7fff1000,µop₂ 从 PRF 读出 0x2a,地址和数据在同一拍内并行汇入 Store Buffer 槽位 #7——这正是 STA/STD 拆分的收益。
拍 5(WB):STA/STD 都不产生寄存器写回,ROB 标记两者"完成"。
拍 6(RET):ROB head #42、#43 按序退休,槽位 #7 标记"可提交"。之后 LSU 异步地把数据写入 L1 D-Cache(拍 N,N≥6)。从程序视角,这条 store 在退休拍正式生效。
写入 L1 D-Cache 没有单独的 µop。 store 的全生命周期只涉及 STA + STD 两条 µop,退休之后的事由 Store Buffer 控制器(LSU 内部状态机)后台完成——按槽位序号排空到 L1,不消耗发射带宽、不占 ROB、不进 RS。唯一能"看到"它的地方:后续同核 load 命中"已退休还没排空"的槽位时,触发 store-to-load forwarding,直接从 store buffer 拿数据,不走 D-Cache。
关键洞察
store 指令的"执行完毕" ≠ "内存已更新"
- 拍 4:执行完毕(Store Buffer 已填充地址和数据)
- 拍 6:退休(槽位标记"可提交",程序视角生效)
- 拍 N(N≥6):真正写入 L1 D-Cache(LSU 异步完成,可能很多拍之后)
★ 多核编程的核心:其他核在"退休"和"写入 D-Cache"之间的窗口期看不到这个 store——这就是 StoreLoad 重排的根源。详见 store-buffer。
案例二:add eax, [rsi+8] —— load + ALU 混合指令
假设条件:
- rsi = 0x7fff2000,eax = 0x10(当前值)
- [rsi+8] = [0x7fff2008] 在 L1 D-Cache 中命中,值 = 0x05
- 指令之前 rsi 和 eax 都已就绪;前一条对 eax 的写已完成(此 add 写的是新 PR)
µop 切分与依赖关系
add eax, [rsi+8]
├── µop₁: LD tmp, [rsi+8] ← Load:从 [rsi+8] 读 4 字节 → 临时物理寄存器 P_new
└── µop₂: ADD eax, tmp ← ALU:eax + tmp → eax(新 PR)
│ │
│ └── 源2:依赖 µop₁ 的结果 P_new(RAW 真依赖!)
└── 源1:读 eax 的当前 PR(P28,已就绪)这是有 RAW 依赖的一对 µop:µop₂ 的源操作数
tmp来自 µop₁ 的结果。µop₂ 必须在 RS 里等 µop₁ 的结果总线广播(tag 匹配)才能发射。这就是为什么 load→use 延迟对性能影响大。
µop 级流水线序列图
下图展示 LD→ADD 这对有 RAW 依赖的 µop 如何穿过流水线各阶段。关键看点:µop₂(ADD) 在 RS 里等待 µop₁(LD) 的结果总线广播,P45 的 tag 从"等待"→"就绪"的那一拍的唤醒逻辑。
![add eax, [rsi+8] (03 46 08) —— load + ALU,带 RAW 依赖(第 2 张) add eax, [rsi+8] (03 46 08) —— load + ALU,带 RAW 依赖(第 2 张)](/plantuml/845f6781dfca954c00cb094b49db449cec84701d.png)
各阶段的关键细节
拍 0-1(IF→ID):add eax, [rsi+8] 编码为 03 46 08(3 字节,ModRM+disp8)。译码器生成 µop₁: LD tmp, [rsi+8] 和 µop₂: ADD eax, tmp。注意 tmp 是译码器自动插入的伪寄存器(pseudo-register)——它在重命名阶段分配真实的物理寄存器(下一拍分配 P45),但不对应任何架构寄存器(RAX/RBX 等的 RAT 表项都不会指向它),程序层面对它完全无感知。它唯一的作用是承载 load 结果,供 ADD µop 消费后立即归还。
拍 2(REN)——RAW 依赖在此建立:
; 译码产物(架构寄存器) → 重命名后(物理寄存器)
; µop₁: LD tmp, [rsi+8] → LD P45, [P22+8] ROB#100
; µop₂: ADD eax, tmp → ADD P46, P28, P45 ROB#101 ← P45 tag 非零!- µop₁:源 rsi→P22(已就绪),目的 tmp 从空闲列表分配 P45;ROB 分配 #100
- µop₂:源1 eax→P28(已就绪),源2 tmp→P45(tag 非零!),目的 eax 分配 P46;ROB 分配 #101
为什么 ADD 重命名后变成了三个操作数? x86 的
add eax, tmp语义是eax = eax + tmp——两操作数,eax 既是源又是目的。但 µop 内部是 RISC 风格三操作数dest = src1 OP src2,原因在寄存器重命名的核心规则:每次写入必须分配新的物理寄存器。eax 的旧值是 P28,不能把加法结果写回 P28(可能有其他飞行中的 µop 还在读 P28),所以分配 P46 作为新目的。所有 x86 的"累加器风格"两操作数指令,在 µop 层面全部展开为三操作数。这也是 PRF 条目数远超架构寄存器数的根本原因——每条写寄存器的 µop 都要消耗一个新的 PR,旧 PR 要等最后一个消费者读完才归还。
重命名的精妙之处:µop₁ 的目的 PR P45 和 µop₂ 的源2 PR P45 是同一个物理寄存器。RAT 在 µop₁ 分配时就建立了
tmp→P45映射,µop₂ 紧接着查 RAT 就拿到了 P45。这就是 RAT 的"写后读"转发——不需要 µop₁ 执行完,µop₂ 就知道"我等的是 P45"。
拍 3(RS):µop₁ 的 P22 就绪,发射到 Port 2 AGU 算地址。µop₂ 的 P45 tag 非零,留在 RS 里等待。
拍 4(LSU):µop₁ 的 LSU 流水线——TLB 翻译 VA→PA,查 Store Buffer 无匹配(无前向可能),查 L1 D-Cache Tag,命中,数据 0x05 本拍末返回。µop₂ 仍在 RS 里空等。
拍 5(WB + RS 唤醒):µop₁ 的结果 0x05 写入 P45,结果总线广播 tag。RS 里的 µop₂ 监听到 P45 就绪→源2 tag 清零→发射到 Port 0 ALU。ALU 通过前推网络直接拿 P45 的值(跳过 PRF 读,省 1 拍)。
拍 6(WB):µop₂ 的结果 0x15 写入 P46,后续依赖 eax 的 µop 被唤醒。
拍 7(RET):ROB #100、#101 同拍退休,eax→P46 正式生效。
load→use 延迟分析
| 事件 | 拍号 | 距 µop₁ 发射的延迟 |
|---|---|---|
| µop₁ 发射到 AGU | 拍 3 | — |
| µop₁ L1 D-Cache 命中,数据返回 | 拍 4 末 | 1 拍 |
| µop₁ 写回 P45,唤醒 µop₂ | 拍 5 | 2 拍 |
| µop₂ 发射到 ALU(前推拿 P45) | 拍 5 | 2 拍 |
| µop₂ 写回 P46 | 拍 6 | 3 拍 |
load→use 最小延迟 = 2 拍(L1 D-Cache 命中 + 前推旁路)。如果 L1 miss 则需额外 12 拍(L2 hit)到数百拍(DDR)。
这 2 拍间隙里,如果 µop₁ 和 µop₂ 之间还有其他不依赖的指令,乱序执行可以用它们填入,掩藏这段延迟。
案例三:addsd xmm0, [rdi+rcx*8] —— 浮点 load + FP 运算,跨集群调度
假设条件:
- rdi = 0x7fff3000, rcx = 3
- xmm0 当前值 = 1.5 (double)
- [rdi + rcx*8] = [0x7fff3018] 在 L1 D-Cache 中命中,值 = 2.5 (double)
- 前一条对 xmm0 的 FP 写已完成
µop 切分与跨集群路径
addsd xmm0, [rdi+rcx*8]
├── µop₁: LD tmp, [rdi+rcx*8] ← Load:AGU 算地址,读 8 字节 → 临时 FP 物理寄存器
└── µop₂: ADDSD xmm0, tmp ← FP ADD:xmm0 + tmp → xmm0(走 FP 执行单元)和案例二的关键区别:µop₁ 走 INT 集群(AGU 在 INT 侧),µop₂ 走 FP 集群(FP ADD 在 FP 侧)。两条 µop 在重命名后分到不同的保留站——µop₁ 进 INT RS,µop₂ 进 FP RS。跨集群的 tag 广播需要额外 1 拍的物理走线延迟。
µop 级流水线序列图
和案例二不同的是,µop₁(LD) 进 INT RS、µop₂(ADDSD) 进 FP RS——两条 µop 分属不同调度域。序列图中 INT RS 和 FP RS 各占一条生命线,跨集群的 tag 广播延迟一目了然。
![addsd xmm0, [rdi+rcx*8] (F2 0F 58 04 CF) —— 跨集群浮点 load + FP ADD(第 3 张) addsd xmm0, [rdi+rcx*8] (F2 0F 58 04 CF) —— 跨集群浮点 load + FP ADD(第 3 张)](/plantuml/c48949940bab13126598de26d1fd3c3cf9c671d0.png)
各阶段的关键细节
拍 0-1(IF→ID):addsd xmm0, [rdi+rcx*8] 编码 5 字节(F2 0F 58 04 CF),带 SIB 字节编码 base+index*scale 寻址。生成 µop₁: LD tmp, [rdi+rcx*8] 和 µop₂: ADDSD xmm0, tmp。
拍 2(REN)——本案例最关键的一拍:
- µop₁:rdi→P50、rcx→P51(都就绪),目的
tmp从 FP 空闲列表分配 FP_PR P12,进 INT RS - µop₂:xmm0→FP_PR P8(就绪),src2
tmp→FP_PR P12(tag 非零),目的 xmm0 分配 FP_PR P13,进 FP RS
两条 µop 进了不同的 RS。INT RS 的 tag 广播和 FP RS 的 tag 匹配走不同的物理总线,µop₁ 在 INT 集群完成 load 后,结果要跨集群广播到 FP 集群才能唤醒 µop₂——这额外增加了 ~1 拍物理走线延迟。
拍 3(INT RS 发射 µop₁):µop₁ 的源操作数全就绪,AGU 算地址 0x7fff3018。µop₂ 在 FP RS 里等待 FP_PR P12。
拍 4-7(µop₁ LSU 查缓存,4 拍):LSU 走 TLB→L1 D-Cache,数据 2.5 在拍 7 末就绪。µop₂ 在 FP RS 里空等这 4 拍。
拍 8(跨集群唤醒):FP_PR P12 写入 2.5,结果总线跨集群广播到 FP RS,µop₂ 被唤醒并发射到 Port 0 FMA。
拍 9-12(FP 流水线,4 拍):FMA 单元内部 4 拍流水线计算 1.5+2.5=4.0。
拍 13(µop₂ WB):FP_PR P13 写入 4.0。
拍 14(RET):ROB #200、#201 同拍退休,xmm0→FP_PR P13 正式生效。
跨集群调度的性能影响
INT 集群 vs FP 集群的调度差异
- 案例二(
add eax, [rsi+8]):两条 µop 都在 INT 集群 → 同一个 RS → tag 广播 0 额外延迟- 案例三(
addsd xmm0, [rdi+rcx*8]):µop₁ 在 INT 集群(AGU),µop₂ 在 FP 集群(FMA)
- 跨集群 tag 广播额外 ~1 拍走线延迟
- 但 FP 执行单元延迟本来就长(4 拍 vs 1 拍 ALU),多 1 拍不敏感
- 真正敏感的是:如果 AGU 和 FP 单元之间需要频繁传递数据(如混合精度计算),跨集群通信成为瓶颈
三条指令全部走读完毕,把它们的时间线叠在一起对比,看规律——三案对比与生命周期总表。