前言
本篇文章记录一下switch语句的底层实现,通过该文章,可以理解下面的问题:
- 我们为什么推荐使用switch语句来进行分支的跳转而不是使用if判断。
- switch语句在什么情况下会比较高效,我们在使用switch语句进行跳转的时候需要注意什么
实例
我们先给出一个简单的例子,看下面的C代码
#include <stdio.h>
void codeHandle(int pCode);
int main(void)
{
codeHandle(2);
}
void codeHandle(int pCode)
{
switch(pCode)
{
case 101:
case 106:
printf("handle 10.");
break;
case 102:
printf("handle 102");
break;
case 103:
printf("handle 103");
break;
case 104:
printf("handle 104");
break;
case 105:
printf("handle 105");
break;
}
}
这是一个处理code的函数,当然处理逻辑我们使用printf函数代替了,按照我们一般的编程思维,在底层机器代码执行时,应该是使用条件跳转指令跳转到正确的分支上执行逻辑
,毕竟机器很善于做这种事情,下面是我们编译好的汇编代码:
codeHandle: // 参数通过%edi传递的
.LFB25:
subl $101, %edi // %edi先减去100
cmpl $5, %edi
ja .L11
subq $8, %rsp
movl %edi, %edi
jmp *.L4(,%rdi,8)
.section .rodata
.align 8
.align 4
.L4:
.quad .L3
.quad .L5
.quad .L6
.quad .L7
.quad .L8
.quad .L3
.text
.L3:
movl $.LC0, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
jmp .L1
.L5:
movl $.LC1, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
jmp .L1
.L6:
movl $.LC2, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
jmp .L1
.L7:
movl $.LC3, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
jmp .L1
.L8:
movl $.LC4, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
.L1:
addq $8, %rsp
.cfi_def_cfa_offset 8
.L11:
rep ret
.cfi_endproc
.LFE25:
.size codeHandle, .-codeHandle
.globl main
.type main, @function
main:
.LFB24:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
movl $2, %edi
call codeHandle
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
可以看到,汇编代码并没有频繁的进行分之判断和跳转,而是在.rodata中生成了一个跳转表,里边保存了所有case的执行逻辑的地址,我们所有的code情况只有101,102,103,104,105,106,所以在函数执行开始先把code减去100,这样就剩了个1,2,3,4,5,6,这正好作为跳转的索引,可以看到因为我们101和106对应的逻辑是一样的,所以跳转表为索引1和索引6创建的跳转目标都是.L3,这样只需要执行jmp *.L4(,%rdi,8)
,就可以直接跳转到需要执行的逻辑代码了,我们可以想见,如果switch分支很多的情况下,switch和if的优势就显而易见了
。下面给出使用if判断的汇编指令
codeHandle:
.LFB25:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
cmpl $101, %edi
sete %dl
cmpl $106, %edi
sete %al
orb %al, %dl
je .L2
movl $.LC0, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
jmp .L1
.L2:
cmpl $102, %edi
jne .L4
movl $.LC1, %esi
movb $1, %dil
movl $0, %eax
call __printf_chk
jmp .L1
.L4:
cmpl $103, %edi
jne .L5
movl $.LC2, %esi
movb $1, %dil
movl $0, %eax
call __printf_chk
jmp .L1
.L5:
cmpl $104, %edi
jne .L6
movl $.LC3, %esi
movb $1, %dil
movl $0, %eax
call __printf_chk
jmp .L1
.L6:
cmpl $105, %edi
jne .L1
movl $.LC4, %esi
movb $1, %dil
movl $0, %eax
call __printf_chk
.L1:
addq $8, %rsp
.cfi_def_cfa_offset 8
ret
.cfi_endproc
通过对比可以看出,使用if跳转比起switch确实多了很多执行逻辑。
对于switch的思考
我们在上面的例子中使用switch时,有一个关键的条件,就是所有code都是挨着的
,这样,生成的跳转表才能通过code作为索引去正确跳转,如果想一下,我们的code间隔很大,比如我们把code=106改成code=206,这样,跳转表应该怎么生成呢。通过测试表明,这种情况下不生成跳转表
codeHandle:
.LFB25:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
cmpl $103, %edi
je .L3
cmpl $103, %edi
jg .L4
cmpl $101, %edi
je .L5
cmpl $102, %edi
.p2align 4,,2
je .L6
.p2align 4,,5
jmp .L1
.L4:
cmpl $105, %edi
.p2align 4,,5
je .L7
cmpl $105, %edi
.p2align 4,,5
jl .L8
cmpl $201, %edi
jne .L1
这是部分汇编代码,可以看到,这时是通过分支判断进行跳转的。
但是,这时我又添加了些分支,并且将分支的间隔调整的差不多一致,代码如下:
void codeHandle(int pCode)
{
switch(pCode)
{
case 100:
case 110:
printf("handle 10.");
break;
case 120:
printf("handle 102");
break;
case 130:
printf("handle 103");
break;
case 140:
printf("handle 104");
break;
case 150:
printf("handle 105");
break;
case 160:
printf("handle 105");
break;
case 170:
printf("handle 105");
break;
case 180:
printf("handle 105");
break;
case 190:
printf("handle 105");
break;
case 200:
printf("handle 105");
break;
}
}
神奇的是跳转表又出来了
codeHandle:
.LFB25:
.cfi_startproc
subq $8, %rsp
.cfi_def_cfa_offset 16
subl $100, %edi
cmpl $100, %edi
ja .L1
movl %edi, %edi
jmp *.L4(,%rdi,8)
.section .rodata
.align 8
.align 4
.L4:
.quad .L3
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L3
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L5
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L6
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L7
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L8
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L9
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L10
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L11
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L12
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L1
.quad .L13
.text
.L3:
movl $.LC0, %esi
movl $1, %edi
movl $0, %eax
call __printf_chk
jmp .L1
后边就省略了,所以最后得出的结论是:
编译器根据开关情况的数量和开关情况值的稀疏程度来翻译开关语句。当开关情况数量比较多,并且值的范围跨度比较小时,就会使用跳转表。
这也就给我们写代码指明了方向
编码规范
在编码过程中使用switch进行分支判断时,尽量使用判断值比较接近的方式,比如可以使用枚举,这样switch会生成跳转表,基本消除了分支判断带来的性能影响,分支判断越多,性能节省越可观。