为什么使用switch

2023-12-13 19:46:37

前言

本篇文章记录一下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会生成跳转表,基本消除了分支判断带来的性能影响,分支判断越多,性能节省越可观。

文章来源:https://blog.csdn.net/b1049112625/article/details/134976597
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。