while(1);的top-down分析

张开发
2026/4/17 3:38:29 15 分钟阅读

分享文章

while(1);的top-down分析
对于简单的while(1)循环int main(){ while(1); return 0;}L1:L2:L3/L4:为什么 Core Bound 是 0%这是最关键的逻辑没有“停顿Stall”就没有“受限Bound”。没有依赖等待jmp指令不依赖任何复杂的计算结果它不需要等待前面的指令算完。没有资源溢出只有一个指令在跑后端的预留站Reservation Station和重命名寄存器永远不会被占满。核心解读Ports_Utilized_1 99.8%含义在 99.8% 的时钟周期里后端有且只有一个端口在执行指令。真相由于你的代码只有一条jmp指令它始终被分配到执行跳转的特定端口。Ports_Utilized_3m同时使用 3 个以上端口是0%while(1);是“绝对串行”每周期只敲一个端口剩下端口全闲着。为什么Ports_Utilization又是 99.8%在 TMAM 模型中Core_Bound.Ports_Utilization的父级指标衡量的是“执行压力是否集中”。因为你的Ports_Utilized_1达到了近乎 100%CPU 认为后端正面临极度的单一端口压力。尽管只有 1 个端口在忙但它“忙得很有规律”。由于这个端口始终处于被占用状态在while(1);这种极简循环里它被定义为 Core Bound 的子项表示“虽然我没卡住0% Backend Bound但我已经被这条单指令压榨到了单端口执行的极限”。如何理解 Frontend Bound 50% 和 Retiring 50%你可能会奇怪既然是while(1);为什么 Retiring 不是 100%机器宽度 vs 指令密度现代 CPU 每个时钟周期可以处理 4 到 8 条指令Slots。但你的while(1);循环体太小了可能每一组时钟周期只能提供 1 或 2 条指令。空置的槽位剩下的槽位因为没有指令可填被标记为Frontend Bound前端没能填满所有槽位。结果这一半的 Slot 退休了50% Retiring另一半空着50% Frontend Bound。总结对于while(1);来说Backend Bound 0%是因为后端处理得太快、太顺畅了完全没排队。IPC是1.99。while(1);对应汇编是2条指令 nop jmp0000000000001129 main: 1129: f3 0f 1e fa endbr64 112d: 55 push %rbp 112e: 48 89 e5 mov %rsp,%rbp 1131: 90 nop 1132: eb fd jmp 1131 main0x8为什么 IPC 是1.99( 2)虽然汇编是nop和jmp但在 Retiring 阶段它们是两条独立的指令。物理执行nop指令在现代 CPU 中通常在前端就被处理掉Nop-elimination或者被分配到一个极其简单的微操作。jmp也是一个微操作。统计结果在一个时钟周期内CPU 退休Retire了一个nop和一个jmp。因此2. 为什么只有一个 Port 在执行这是最关键的一点。在 Skylake 微架构中nop不占用执行端口现代 CPU 拥有“消除 NOP”的能力。当nop到达重命名Rename或分配Allocate阶段时CPU 直接标记它已完成而不需要把它发往任何执行端口。jmp占用1个port只有jmp真正需要进入执行后端并敲击端口。结论在 99.8% 的时钟周期里只有一个物理微操作jmp需要执行所以Ports_Utilized_1是 100%。如果现在代码做4 个独立的加法操作完全发挥后端多个ALU 端口的实力并尽可能消除了内存访问和分支干扰int main() { // 使用寄存器变量建议编译器不要写回内存 register long a 0, b 0, c 0, d 0; while (1) { // 使用内联汇编确保生成最纯粹的指令流 // 1. 展开循环减少 jmp 指令占用的槽位比例 // 2. 独立寄存器确保 4 条指令可以同时分配给 4 个不同的执行端口 __asm__ __volatile__ ( add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; add $1, %[a]; add $1, %[b]; add $1, %[c]; add $1, %[d]; : [a] r (a), [b] r (b), [c] r (c), [d] r (d) : : cc ); } return 0;}结果变成L1:frontend bound消除backend bound到15.7%84.2%的时间在retiring指令。L2:backend bound主要是core bound不是memory bound因为我们用的register变量。L3/L4:核心指标Ports_Utilized_3m (93.3%)这表示在93.3%的时钟周期里后端执行单元Execution Units正同时有3 个或更多的端口在发射DispatchuOps。你的代码add a; add b; add c; add d; jmp;硬件行为Skylake 有 4 个 ALU 端口0, 1, 5, 6。因为你的加法在不同寄存器上没有数据依赖所以 CPU 可以真正地在同一个周期把这 4 个add发射到这 4 个不同的端口去。结论这个 93.3% 证明了你极大地利用了指令级并行ILP。你的后端基本没有闲着的时候。对比Ports_Utilized_3m同时使用 3 个以上端口是 93.3%之前while(1);则是0%。while(1);是“绝对串行”每周期只敲一个端口剩下端口全闲着。展开后的代码是“高度并行”每周期大家一起干活。IPC:insn per cycle是3.37。

更多文章