关于switch的讨论

无意在网上看到一篇关于if与switch的问题帖子,其中题主提问了关于为什么同一种逻辑实现, 用if写和用switch写得到的结果却不同。

关于这点我一开始是不信的,代码写旧了总会自己预设一些假设,同时一些已经实现的功能或代码也总是抱着100%信任的态度,但是这往往就造成了许多意想不到的bug的出现。因此本着实事求是的原则,还是做了这么个实验。先用if来实现一个分支选择的功能。

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char **argv)
{
int i = 1,j = 0;
if (i == 1)
j = 1;
else if (i == 2)
j = 2;
else if (i == 3)
j = 3;
else
j = 4;
}

编译这段代码(编译器Apple clang version 11.0.0 (clang-1100.0.33.8);Target: x86_64-apple-darwin19.0.0),得到如下汇编指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
0000000000000000 <_main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
b: 89 7d f8 mov %edi,-0x8(%rbp)
e: 48 89 75 f0 mov %rsi,-0x10(%rbp)
12: c7 45 ec 01 00 00 00 movl $0x1,-0x14(%rbp)
19: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)
20: 83 7d ec 01 cmpl $0x1,-0x14(%rbp)
24: 0f 85 0c 00 00 00 jne 36 <_main+0x36>
2a: c7 45 e8 01 00 00 00 movl $0x1,-0x18(%rbp)
31: e9 3d 00 00 00 jmpq 73 <_main+0x73>
36: 83 7d ec 02 cmpl $0x2,-0x14(%rbp)
3a: 0f 85 0c 00 00 00 jne 4c <_main+0x4c>
40: c7 45 e8 02 00 00 00 movl $0x2,-0x18(%rbp)
47: e9 22 00 00 00 jmpq 6e <_main+0x6e>
4c: 83 7d ec 03 cmpl $0x3,-0x14(%rbp)
50: 0f 85 0c 00 00 00 jne 62 <_main+0x62>
56: c7 45 e8 03 00 00 00 movl $0x3,-0x18(%rbp)
5d: e9 07 00 00 00 jmpq 69 <_main+0x69>
62: c7 45 e8 04 00 00 00 movl $0x4,-0x18(%rbp)
69: e9 00 00 00 00 jmpq 6e <_main+0x6e>
6e: e9 00 00 00 00 jmpq 73 <_main+0x73>
73: 8b 45 fc mov -0x4(%rbp),%eax
76: 5d pop %rbp
77: c3 retq

从总可以很容易看出if指令在汇编级是由cmpl与jne组合实现的。

那么再看switch又有什么区别呢?如下为用switch实现的分支选择。

1
2
3
4
5
6
7
8
9
10
int main(int argc, char **argv)
{
int i = 1,j = 0;
switch (i) {
case 1: j = 1; break;
case 2: j = 2; break;
case 3: j = 3; break;
default: j = 4; break;
}
}

switch实现与if实现逻辑功能完全相同,但汇编级实现却不一样,其反汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
0000000000000000 <_main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
b: 89 7d f8 mov %edi,-0x8(%rbp)
e: 48 89 75 f0 mov %rsi,-0x10(%rbp)
12: c7 45 ec 01 00 00 00 movl $0x1,-0x14(%rbp)
19: c7 45 e8 00 00 00 00 movl $0x0,-0x18(%rbp)
20: 8b 7d ec mov -0x14(%rbp),%edi
23: 89 f8 mov %edi,%eax
25: 83 e8 01 sub $0x1,%eax
28: 89 7d e4 mov %edi,-0x1c(%rbp)
2b: 89 45 e0 mov %eax,-0x20(%rbp)
2e: 0f 84 2d 00 00 00 je 61 <_main+0x61>
34: e9 00 00 00 00 jmpq 39 <_main+0x39>
39: 8b 45 e4 mov -0x1c(%rbp),%eax
3c: 83 e8 02 sub $0x2,%eax
3f: 89 45 dc mov %eax,-0x24(%rbp)
42: 0f 84 25 00 00 00 je 6d <_main+0x6d>
48: e9 00 00 00 00 jmpq 4d <_main+0x4d>
4d: 8b 45 e4 mov -0x1c(%rbp),%eax
50: 83 e8 03 sub $0x3,%eax
53: 89 45 d8 mov %eax,-0x28(%rbp)
56: 0f 84 1d 00 00 00 je 79 <_main+0x79>
5c: e9 24 00 00 00 jmpq 85 <_main+0x85>
61: c7 45 e8 01 00 00 00 movl $0x1,-0x18(%rbp)
68: e9 1f 00 00 00 jmpq 8c <_main+0x8c>
6d: c7 45 e8 02 00 00 00 movl $0x2,-0x18(%rbp)
74: e9 13 00 00 00 jmpq 8c <_main+0x8c>
79: c7 45 e8 03 00 00 00 movl $0x3,-0x18(%rbp)
80: e9 07 00 00 00 jmpq 8c <_main+0x8c>
85: c7 45 e8 04 00 00 00 movl $0x4,-0x18(%rbp)
8c: 8b 45 fc mov -0x4(%rbp),%eax
8f: 5d pop %rbp
90: c3 retq

从反汇编结果来看,20-5c部分为switch的条件码计算、比较和跳转的逻辑,而61-85则是case中包含的执行语句。

通过比较这两种实现的汇编代码,可以得到if与switch的几点重大区别。

  1. if的条件码比较和分支过程是紧临的,switch的条件码比较和分支执行过程是松散的。
  2. if的条件码比较和跳转使用cmpl指令和jne指令,switch的条件码比较与跳转使用的是sub,je指令
  3. if的比较过程不改变源操作数的结果仅改变状态寄存器的值,switch的比较过程会改变源操作数的结果和状态寄存器的值,因此需要保存源操作数,从以上汇编代码可知每次都会将比较的原值与计算的结果存入堆栈。

对于if与switch在汇编级的区别,结果是显然的了,但是从逻辑上来看它们的实现都是正确的,因此在这里没能看出上面帖子所出现的问题的根本原因,但是从上面分析中知道,如果在堆栈容量不足的情况下,if的实现绝对是优先的选择,因为switch的实现需要占用更多的堆栈空间,在堆栈容量不足时确实存在堆栈溢出的可能,从而造成内存相关的错误甚至一些未知错误。

同时在内核中也有很多地方选择if而不选择switch,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Can't use a switch () here: gcc isn't always smart enough for that... */
if ((i) == -16)
IA64_FETCHADD(_tmp, _v, -16, sizeof(*(v)), sem);
else if ((i) == -8)
IA64_FETCHADD(_tmp, _v, -8, sizeof(*(v)), sem);
else if ((i) == -4)
IA64_FETCHADD(_tmp, _v, -4, sizeof(*(v)), sem);
else if ((i) == -1)
IA64_FETCHADD(_tmp, _v, -1, sizeof(*(v)), sem);
else if ((i) == 1)
IA64_FETCHADD(_tmp, _v, 1, sizeof(*(v)), sem);
else if ((i) == 4)
IA64_FETCHADD(_tmp, _v, 4, sizeof(*(v)), sem);
else if ((i) == 8)
IA64_FETCHADD(_tmp, _v, 8, sizeof(*(v)), sem);
else if ((i) == 16)
IA64_FETCHADD(_tmp, _v, 16, sizeof(*(v)), sem);
else \
_tmp = __bad_increment_for_ia64_fetch_and_add();
(__typeof__(*(v))) (_tmp); /* return old value */

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 yxhlfx@163.com

文章标题:关于switch的讨论

本文作者:红尘追风

发布时间:2019-02-13, 22:49:46

原始链接:http://www.micernel.com/2019/02/13/%E5%85%B3%E4%BA%8Eswitch%E7%9A%84%E8%AE%A8%E8%AE%BA/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录