3.1 分支与跳转:if 与 switch 的汇编真相


3.1 分支与跳转:if 与 switch 的汇编真相

本节摘要:CPU 没有结构化编程的概念,分支的全部实现是比较指令加条件跳转。本节看 if/else 与 switch 的真实汇编形态,解释跳转表优化的触发条件、短路求值的指令展开,以及分支预测失败惩罚如何影响热路径性能——读完你会明白"分支不是免费的"这句话的精确含义。

if/else:一条比较加一条条件跳转

从最简单的分支开始:

int grade(int score) { if (score >= 60) return 1; return 0; }

关闭优化编译后的 x86-64 汇编骨架:

grade: push rbp mov rbp, rsp cmp DWORD PTR [rbp-8], 59 ; 与 59 比较 jle .L2 ; 小于等于则跳去返回 0 mov eax, 1 ; 否则返回 1 jmp .L3 .L2: mov eax, 0 .L3: pop rbp ret

三步曲:比较(cmp)、条件跳转(jle)、无条件跳转(jmp)。else 的本质是"两个出口汇合"。所有 if/else 不管嵌套多少层,都是这三步的嵌套展开——这是结构化语法糖下面的真实面貌。

一个值得知道的细节:score >= 60 被编译成 cmp 59; jle,比较与跳转合并处理"大于等于"语义。CPU 没有几百种比较指令,只有少量标志位(零位、符号位、进位等)配合一组条件跳转。cmp 只负责"算出标志位",jle 负责"看标志位决定跳不跳"——比较与决策是指令层的两个动作。

switch:从逐个比较到一张跳转表

分支少于四五个时,switch 基本按 if 链翻译。但当 case 值密集且数量多时,编译器会换武器——跳转表:

const char *weekday(int d) { switch (d) { case 1: return "Mon"; case 2: return "Tue"; case 3: return "Wed"; case 4: return "Thu"; case 5: return "Fri"; default: return "?"; } }

编译后的骨架(示意):

weekday: cmp edi, 5 ; 超出 1..5 ja .default mov eax, edi jmp [table + eax*8] ; 查表一次直达 .default: ... table: .quad .case1 .quad .case2 .quad .case3 .quad .case4 .quad .case5

一次范围检查加一次查表跳转,不管 5 个分支还是 500 个分支,代价几乎恒定。代价是那张表本身要占空间——所以值稀疏时(比如 case 1、100、10000)编译器会退回比较链或二分结构。ja(无符号大于跳转)在这里一箭双雕:负数按无符号看是巨大值,一次比较同时挡住负数与超界——第 2 章"同一字节两副面孔"的实战演出。

图 1 两种分支形态的机器对照

图 1 两种分支形态的机器对照

短路求值:逻辑运算的秘密身份

&&|| 不是"运算",是带跳转的语法

if (p != NULL && p->count > 0) { ... }

汇编形态:p 为空时直接跳过第二个条件——不评估、不访问内存。这叫短路求值,它让"先判空再解引用"这种防御代码零额外成本地串在一起。也正因此有两个工程含义:

  1. 依赖短路顺序的代码是合法且惯用的(上面这行就是标准写法);
  2. 但别把有副作用的表达式放在 && 右边又依赖它一定执行——if (ok && ++counter > 3)ok 为假时 counter 不会自增,这种隐蔽性 bug 查起来很费眼。

位运算 &| 没有短路:两边永远都算。写性能敏感且无短路需要的代码时,有经验的工程师偶尔故意用 & 连接两个比较(if ((a > 0) & (b > 0))),让 CPU 走无分支的位运算路径——这引出下一个话题。

分支预测:猜对了免费,猜错了罚款

现代 CPU 流水线深达十几级,遇到条件跳转时它不等结果,先猜一个方向继续执行。猜对无事发生;猜错则清空流水线重来,白白浪费十几个周期。命中率极高时(典型场景:99% 走同一路径的检查判断)分支近乎免费;命中率接近一半时(数据随机分布的 if-else)惩罚显著。

经典对照:把一个无符号数清零化分支的循环——

for (int i = 0; i < n; i++) if (data[i] >= 128) sum += data[i];

先花时间把 data 排序再跑这个循环,总时间常常反而更短——排序的代价换来了分支预测命中率飙升。这是"分支惩罚真实存在"最著名的实验。规避手段也朴素:能变成算术/位运算的分支就变(条件移动、掩码),热循环里的随机分支尽量消除。

⚠️ 常见坑:在 switch 里忘记 break 导致"贯穿"——下一个 case 的代码接着执行。贯穿偶尔是刻意技巧,但 99% 是事故。刻意贯穿时请写注释声明,否则审阅者会当成 bug 修掉。

💡 关键直觉:写分支时想着两件事——这个 switch 的 case 值密集吗(决定跳转表)、这个分支在热路径上可预测吗(决定预测惩罚)。分支结构的选择是真实的性能决策,不只是代码风格。

本节要点回顾

  • 分支的机器三步曲:比较出标志位、条件跳转、汇合;else 只是两个出口。
  • switch 密集时建跳转表:一次范围检查加一次查表,分支数几乎不影响代价;稀疏时退回比较链。
  • 短路求值是带跳转的语法:判空防御串写零成本;副作用别藏在右边。
  • 分支预测猜错罚款十几个周期:随机分支的热循环是性能黑洞,可排序或改算术消除。
  • 忘 break 的贯穿是高频事故:刻意贯穿必须注释声明。

下一节把"跳一次"变成"跳回一次"——循环,并引入现代性能第一课:缓存。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U