while(1);的top-down分析

对于简单的while(1)循环:

cs 复制代码
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 + jmp:

apache 复制代码
0000000000001129 <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 <main+0x8>

为什么 IPC 是1.99( 2)?

虽然汇编是 nopjmp,但在 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 端口的实力,并尽可能消除了内存访问和分支干扰:

php 复制代码
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 个或更多的端口 在发射(Dispatch)uOps。

  • 你的代码: 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。