switch-case的性能优化策略
【起源】
听到有人讨论if-else的执行效率,当分支较多时,if-else 往往需要逐个判断条件,最坏情况可能导致线性(O(N))的比较开销。switch-case 同样是一种控制流语句。有人说它的执行效率比 if-else 更高,但这是真的吗?为什么有这样的说法?不清楚。 switch 是如何确定跳转目标的?不清楚。
if倒是很好猜,通过一个一个条件判断进行比较,那switch-case呢?
在 C++ 开发中,switch-case 是常见的控制流语句之一。虽然它看似简单,但背后编译器的优化方式却多种多样,对程序性能有显著影响。本文从性能优化的角度出发,深入剖析 switch-case 的三种主要实现方式:线性比较(if-else 风格)、跳表(jump table) 和 二分查找(binary search tree),并提供示例与编译器生成汇编的分析。
【碎碎念】
所以switch-case的实现方式并不是只有一种,编译器会根据不同场景生成不同的汇编代码,那这几种汇编生成方式原理是什么?会有什么效率差异?
背景
汇编分析的必要性
同一段高级语言代码,在不同编译器、目标平台或优化等级下,可能会生成截然不同的汇编代码。
由于汇编语言是对机器语言的直接映射,仅比其抽象一层,因此能够清晰地展现程序在指令层级的真实执行方式。
通过分析汇编代码,我们可以在一定程度上评估程序的执行效率,定位性能瓶颈。
快速生成汇编代码
switch-case 基本语法结构
// 代码示例
int switch_dense(int x) {
switch (x) {
case 0: return x + 100;
case 1: return x + 11;
case 2: return x + 1002;
case 3: return x + 1053;
case 4: return x + 104;
case 5: return x + 105;
case 6: return x + 106;
case 7: return x + 107;
case 8: return x + 108;
case 9: return x + 109;
default: return -1;
}
}
这是经典的switch-case写法,从源码层面写的都是switch-case,但是编译器会根据 case 值的数量和分布特征选择不同的优化策略,从而生成不同的汇编语言。
代数归纳优化
这里每一个case中的计算内容会尽量写的没有规律,不然编译器可能会有代数归纳优化,从而掩盖跳表/二分查找优化。
这里简单说一下代数归纳优化int switch_dense(int x) { switch (x) { case 0: return 100; case 1: return 200; case 2: return 300; } return -1; }上述代码在汇编层面会直接优化为:
switch_dense2(int): cmp edi, 2 ja .L13 add edi, 1 imul eax, edi, 100 ret .L13: mov eax, -1 ret简单描述就是
若输入大于2,直接跳转到.L13,返回-1 若不是,则返回(x+1)*100多个返回值因为过于规律直接被优化成了公式。
编译器的三种汇编生成策略
跳表(jump table)
特点
当 case 数值连续或接近连续的情况,也就是case数值为稠密的情况,通常超过 4~5 个连续 case,在设置优化级别大于-O2时GCC/Clang 会倾向于生成跳表。
每一个编译器都有自己的稠密标准判断,这里详细说明一下怎么计算,假设源码中case是顺序的
switch(x) { case A: xxx ................... case B: xxx }A~B之间的case数量为n,n/(B-A + 1)就可以知道case的稠密程度,这个稠密程序会让编译器决定是否选择跳表,太稀疏的话就不会。
什么是跳表
- 即函数指针数组,通过索引直接跳转。
- 时间复杂度为 O(1),但空间占用较大(尤其稀疏时浪费空间)。
案例及说明
源代码
int switch_dense(int x) {
switch (x) {
case 0: return x + 100;
case 1: return x + 11;
case 2: return x + 1002;
case 3: return x + 1053;
case 4: return x + 104;
case 5: return x + 105;
default: return -1;
}
}
使用godbolt生成的汇编代码,可以看到使用跳表之后非常快,当知道x的值是多少之后,直接根据偏移跳转到了对于的执行电,避免了条件判断,能达到O(1)的速度。
switch_dense(int):
cmp edi, 5 ; 判断参数 x 是否 > 5(case 最大值)
ja .L13 ; 如果 x > 5,跳转到 default 分支
; 否则说明 0 <= x <= 5,可以使用跳表快速定位
mov edi, edi
; 加载跳表地址 + 偏移:eax = *(CSWTCH.7 + 4 * x)
mov eax, DWORD PTR CSWTCH.7[0+rdi*4]
ret ; 返回 eax 中的结果
.L13:
mov eax, -1 ; default: 返回 -1
ret
................................
................................
; 跳表数据区,一般位于 .rodata 或 .text 中
CSWTCH.7:
.long 100 ; case 0: return 0 + 100 = 100
.long 12 ; case 1: return 1 + 11 = 12
.long 1004 ; case 2: return 2 + 1002 = 1004
.long 1056 ; case 3: return 3 + 1053 = 1056
.long 108 ; case 4: return 4 + 104 = 108
.long 110 ; case 5: return 5 + 105 = 110
如果不是这种紧凑的,而是中间有空位的,例如把
case 4的情况去掉,那么一样会生成跳表,只不过在对于的x == 4对于的位置直接返回-1CSWTCH.7: .long 100 .long 12 .long 1004 .long 1056 .long -1 ; x == 4 .long 110
二分查找
特点
- 编译器生成按值划分的比较树,类似手写二分。
- 时间复杂度 O(log n),空间开销小于跳表,运行速度快于线性判断。
案例
int baz(int x) {
switch (x) {
case 1: return 10;
case 5: return 50;
case 10: return 100;
case 20: return 200;
case 40: return 400;
case 80: return 800;
default: return -1;
}
}
baz(int):
cmp edi, 20
je .L17
jg .L16
mov eax, 50
cmp edi, 5
je .L14
mov eax, 100
cmp edi, 10
je .L14
cmp edi, 1
mov edx, 10
mov eax, -1
cmove eax, edx
ret
.L16:
mov eax, 400
cmp edi, 40
je .L14
cmp edi, 80
mov edx, 800
mov eax, -1
cmove eax, edx
ret
.L17:
mov eax, 200
.L14:
ret
顺序判断(if-else 风格)
特点
- 按顺序生成多个 cmp + je/jne 语句。
- 时间复杂度为 O(n)。
案例
不开优化一般就是顺序判断
int switch_func(int x) {
switch (x) {
case 0: return x + 100;
case 1: return x + 11;
case 2: return x + 1002;
default: return -1;
}
}
汇编代码,就是一个一个顺序比较下去
switch_func(int):
push rbp
mov rbp, rsp
mov DWORD PTR [rbp-4], edi
cmp DWORD PTR [rbp-4], 2
je .L2
cmp DWORD PTR [rbp-4], 2
jg .L3
cmp DWORD PTR [rbp-4], 0
je .L4
cmp DWORD PTR [rbp-4], 1
je .L5
jmp .L3
.L4:
mov eax, DWORD PTR [rbp-4]
add eax, 100
jmp .L6
.L5:
mov eax, DWORD PTR [rbp-4]
add eax, 11
jmp .L6
.L2:
mov eax, DWORD PTR [rbp-4]
add eax, 1002
jmp .L6
.L3:
mov eax, -1
.L6:
pop rbp
ret
总结
开优化,混着用
一般编译器不会局限于一种方式,在一个switch中有可能会几种方式混着用,不过一般只要开了优化,最差也是二分查找,如果有一段case比较密集,还会跳表和二分法混着用。
建议
- 尽量使用连续且小范围的 case 值。这样编译器倾向于使用跳转表(jump table)。
- 开启编译器优化选项
1万+

被折叠的 条评论
为什么被折叠?



