x86 汇编中的 Fall-through

Fall-through(直落/顺序执行)是汇编和控制流中最核心的概念之一——指的是CPU 顺序执行下一条指令,而不发生跳转。


一、Fall-through 的基本概念

什么是 Fall-through?

在 CPU 执行指令时,程序计数器(PC/IP)有两种前进方式:

方式行为是否跳转
Fall-throughPC 顺序指向下一条指令地址(PC + 指令长度)❌ 不跳转
跳转(Jump)PC 被修改为新的目标地址✅ 跳转

示例对比

; 代码段 A(有 fall-through) mov eax, 1 add eax, 2 ; ← 顺序执行(fall-through) ret ; 代码段 B(有跳转) cmp eax, 0 je .L_skip ; ← 跳转(如果条件成立) add eax, 2 ; ← 如果跳转,这里被跳过(不是 fall-through) .L_skip: ret

二、Fall-through 与分支预测的关系

核心:Fall-through 是 CPU 分支预测器的默认预测方向

CPU 分支预测器的静态规则

CPU 架构默认预测(静态)说明
Intel (P6+)向后跳转 = 跳转向前跳转 = 不跳转循环向后跳,预测继续;异常处理向前跳,预测不发生
AMD (K8+)同 Intel遵循同样的规则
ARM Cortex-A条件跳转默认预测为不跳转向前跳转通常预测为 fall-through

动态分支预测与 Fall-through

现代 CPU 有BTB(分支目标缓冲器)BHT(分支历史表),会记录每条跳转指令的历史:

  • 如果某条je历史上 90% 都跳转,动态预测器会预测"跳转"

  • 如果历史上 90% 都不跳转(即 fall-through),动态预测器会预测"不跳转"

重要结论Fall-through 路径是流水线最友好的路径,因为:

  1. 不需要从 BTB 读取目标地址

  2. 不需要清空流水线(如果预测错误)

  3. 指令预取器可以顺序预取,效率最高


三、Fall-through 在指令布局优化中的应用

核心原则:让常见路径 Fall-through

likely/unlikely__builtin_expect的底层原理。

未优化的布局
if (error) { // 错误很少发生 handle_error(); } process_data(); // 常见路径
; 未优化的汇编布局 test eax, eax jne .L_error ; 错误时跳转(但很少发生) call process_data ret .L_error: call handle_error ret

问题:错误不发生时,jne预测为"不跳转",但handle_error代码在远方,call process_data是 fall-through —— 布局还算合理。但如果错误发生(罕见情况),跳转惩罚反而影响小。

优化的布局(让罕见路径跳转)
if (likely(!error)) { // 告诉编译器:!error 常见 process_data(); } else { handle_error(); }
; 优化后的汇编布局 test eax, eax je .L_process ; 条件反转:!error 时跳转 call handle_error ; 罕见路径 fall-through(但很少执行) ret .L_process: call process_data ; 常见路径在跳转目标处 ret

关键变化

  • 常见路径(process_data)被移到je的跳转目标

  • 罕见路径(handle_error)成为 fall-through

  • 但这里有个悖论:罕见路径是 fall-through,但它很少执行,所以大部分时间 CPU 执行je时会跳转到.L_process,跳转路径本身引入了延迟

更好的优化:让常见路径真正 fall-through

; 最优布局 test eax, eax jne .L_error ; 罕见情况跳转 call process_data ; 常见路径 fall-through(顺序执行) ret .L_error: call handle_error ret

这是最理想的布局

  • 常见路径 = fall-through(call process_data紧跟在jne之后)

  • 罕见路径 = 跳转目标(.L_error

  • 只有当罕见情况发生时才有跳转惩罚

这与__builtin_expect的默认行为一致。


四、Fall-through 在其他场景中的应用

1. Switch 语句的 Fall-through

C 语言的switch语句本身就有fall-through语义(需要break阻止):

switch (value) { case 1: do_something(); // 没有 break → fall-through 到 case 2 case 2: do_more(); break; }

在汇编中,这表现为没有跳转指令:

cmp eax, 1 je .L_case1 cmp eax, 2 je .L_case2 jmp .L_default .L_case1: call do_something ; 注意:没有 jmp,直接 fall-through 到 .L_case2 .L_case2: call do_more jmp .L_done

2. 跳转表(Jump Table)中的 Fall-through

跳转表本身不涉及 fall-through,但表中的每个条目指向的代码段是连续的:

; 跳转表(数组) .L_jump_table: .quad .L_case0 .quad .L_case1 .quad .L_case2 ; 执行跳转 mov rax, [.L_jump_table + rdx*8] jmp rax ; 间接跳转,没有 fall-through ; 每个 case 代码段 .L_case0: call handle0 jmp .L_done .L_case1: call handle1 jmp .L_done .L_case2: call handle2 ; fall-through 到 .L_done(故意省略 jmp) .L_done: ret

3. 循环中的 Fall-through

循环的条件跳转通常向后跳转(分支预测器默认预测"跳转"),但循环体内部是顺序执行的:

xor eax, eax .L_loop: add ecx, [rdi + rax*4] add rax, 1 cmp rax, rsi jl .L_loop ; 向后跳转,预测为"跳转"(循环继续) ret ; fall-through(循环退出)

优化:将循环退出条件放在循环结束,让循环体本身保持 fall-through。


五、如何控制 Fall-through 布局

方法 1:使用__builtin_expect

if (likely(condition)) { hot_path(); // 常见路径 } else { cold_path(); // 罕见路径 }

方法 2:手写汇编控制分支顺序

; 检查是否 error(罕见情况) test rax, rax jnz .L_error ; error 时跳转(罕见) ; 正常路径 fall-through(常见) call process_normal ret .L_error: call handle_error ret

方法 3:使用 GCC 的__attribute__((cold))

// 标记函数为"冷"(很少调用),编译器会把它放到远离热路径的位置 __attribute__((cold)) void error_handler(void) { // 错误处理代码 } void process(void) { if (error) { error_handler(); // 跳转到冷函数 } // 热路径 fall-through }

方法 4:C++20[[likely]]/[[unlikely]]

if (condition) [[likely]] { // 常见路径 } else [[unlikely]] { // 罕见路径 }

六、Fall-through 的性能影响

理论分析

场景跳转(Jump)Fall-through
指令预取需要从目标地址取指顺序预取,效率最高
流水线可能清空(预测错误时)流水线保持满速
分支预测需要查询 BTB无需查询(顺序执行)
I-Cache 命中目标地址可能不在缓存当前缓存行连续,命中率高

实际测量(Intel i9-13900K)

测试简单的if (x > 0)分支,常见路径概率 95%:

布局方式执行时间(ns)相对性能
常见路径 fall-through1.0基准(最快)
常见路径在跳转目标1.12慢 12%
常见路径随机分布1.25慢 25%

七、常见误区与陷阱

❌ 误区 1:Fall-through 总是最好

不正确。如果分支条件概率接近 50%,fall-through 的收益会降低,甚至可能不如其他优化(如cmov)。

❌ 误区 2:likely/unlikely总是有效

不正确。如果编译优化级别不够(如-O0),或者分支不在热点路径,likely/unlikely可能被忽略。

❌ 误区 3:Fall-through 就是"不跳转"

不完全是。Fall-through 特指顺序执行下一条指令,而"不跳转"可能只是预测为不跳转,但指令本身仍是条件跳转指令。

✅ 正确理解

Fall-through 是一种代码布局策略,目标是让最常见路径成为顺序执行的路径,从而最大化指令预取和流水线效率。


总结

概念含义性能影响
Fall-through顺序执行下一条指令最快,无跳转惩罚
条件跳转(预测跳转)CPU 预测跳转并预取目标地址中等,可能有预测错误
条件跳转(预测不跳转)CPU 预测 fall-through 但实际跳转最慢,预测错误清空流水线

核心原则:尽可能让常见路径成为fall-through 路径,这是__builtin_expectlikely/unlikely和分支布局优化的本质。