C语言的 switch 语句的底层汇编具体实现(以ARM汇编为例讲解)

我于昨晚去世,走时心如止水。我于今早重生,来时心怀暖阳。敬你岁月无波澜,祝我余生不悲欢!
散文集 - 《我在人间凑数的日子》

一、参考资料

ARM M3/M4汇编指令TBB TBH实现复杂表格跳转

C语言switch语句的汇编语言实现

C语言汇编代码分析(switch case)

本文是在看大佬们的文章后自己总结的,感谢大佬们的文章分享。

二、具体分析

我们知道 C 语言的 switch 关键字可以让一些情况下的分支判断变得更简洁,可以避免大量使用 if-else if-else不断判断的情况,让代码看起来更简洁。

那么问题来了,switchif 语句间到底在底层实现上有什么区别呢?

直接说结论,其实答案还不是唯一,和编译器实现还有具体的 switch 有关,下面我们分别讨论。

以下以 ARM 汇编为例,别的架构的编译器可能会有不同的结果,不过应该大同小异。

2.1、case 数量情况少且 case 值比较连续的看情况

C代码:

switch (mode)
{case 0:count = 1;break;case 1:count = 2;break;case 2:count = 4;break;case 3:count = 4;break;default :break;
}

看汇编实现:

    51:         switch (mode) 52:         { 53:             case 0: 
0x0800018A B135      CBZ      r5,0x0800019A
0x0800018C 2D01      CMP      r5,#0x01
0x0800018E D006      BEQ      0x0800019E
0x08000190 2D02      CMP      r5,#0x02
0x08000192 D006      BEQ      0x080001A2
0x08000194 2D03      CMP      r5,#0x03
0x08000196 D108      BNE      0x080001AA
0x08000198 E005      B        0x080001A654:                 count = 1; 
0x0800019A 2401      MOVS     r4,#0x0155:                 break; 56:              57:             case 1: 
0x0800019C E006      B        0x080001AC58:                 count = 2; 
0x0800019E 2402      MOVS     r4,#0x0259:                 break; 60:              61:             case 2: 
0x080001A0 E004      B        0x080001AC62:                 count = 4; 
0x080001A2 2404      MOVS     r4,#0x0463:                 break; 64:              65:             case 3: 
0x080001A4 E002      B        0x080001AC66:                 count = 4; 
0x080001A6 2404      MOVS     r4,#0x0467:                 break; 68:              69:             default : 
0x080001A8 E000      B        0x080001AC70:                 break; 71:         } 

我们看到使用了非常多的 CMP 指令,其实在这种情况下和使用多个 if-else if 一样。

2.2、case 数量情况多且 case 值比较连续的看情况

C 代码:

switch (mode)
{case 0:count = 1;break;case 1:count = 2;break;case 2:count = 4;break;case 3:count = 4;break;case 4:count = 1;break;case 6:count = 2;break;case 8:count = 4;break;case 10:count = 4;break;default :break;
}

我们来看下汇编:

    51:         switch (mode) 52:         { 53:             case 0: 
0x0800018A 2D0B      CMP      r5,#0x0B
0x0800018C D217      BCS      0x080001BE
0x0800018E E8DFF005  TBB      [pc,r5]
0x08000192 0806      DCW      0x0806
0x08000194 0C0A      DCW      0x0C0A
0x08000196 160E      DCW      0x160E
0x08000198 1610      DCW      0x1610
0x0800019A 1612      DCW      0x1612
0x0800019C 0014      DCW      0x001454:                 count = 1; 
0x0800019E 2401      MOVS     r4,#0x0155:                 break; 56:              57:             case 1: 
0x080001A0 E00E      B        0x080001C058:                 count = 2; 
0x080001A2 2402      MOVS     r4,#0x0259:                 break; 60:              61:             case 2: 
0x080001A4 E00C      B        0x080001C062:                 count = 4; 
0x080001A6 2404      MOVS     r4,#0x0463:                 break; 64:              65:             case 3: 
0x080001A8 E00A      B        0x080001C066:                 count = 4; 
0x080001AA 2404      MOVS     r4,#0x0467:                 break; 68:  69:             case 4: 
0x080001AC E008      B        0x080001C070:                 count = 1; 
0x080001AE 2401      MOVS     r4,#0x0171:                 break; 72:              73:             case 6: 
0x080001B0 E006      B        0x080001C074:                 count = 2; 
0x080001B2 2402      MOVS     r4,#0x0275:                 break; 76:              77:             case 8: 
0x080001B4 E004      B        0x080001C078:                 count = 4; 
0x080001B6 2404      MOVS     r4,#0x0479:                 break; 80:              81:             case 10: 
0x080001B8 E002      B        0x080001C082:                 count = 4; 
0x080001BA 2404      MOVS     r4,#0x0483:                 break;             84:             default : 
0x080001BC E000      B        0x080001C085:                 break; 86:         } 87:     } 

我们可以看到 TBB(ARM M3/M4汇编指令TBB TBH实现复杂表格跳转)这个汇编指令, 这个指令用于 switch 跳转,其实现方式有些类似数组,我们将程序中的 case 值作为数组的索引,就可以立即得到目标值。

这种方式是采用空间换时间的方式,在占用更多内存的情况下,我们可以快速匹配到要执行的语句。

值得注意的是这里只要求 case 比较连续即可,即使这里举例用的是 100-104。这点也可以类比数组,将其映射到 0-4 即可。

2.3、 case 值跨度比较大的看情况

看 C 代码,只是将上文的 case 10 改为 case 50

switch (mode)
{case 0:count = 1;break;case 1:count = 2;break;case 2:count = 4;break;case 3:count = 4;break;case 4:count = 1;break;case 6:count = 2;break;case 8:count = 4;break;case 50:count = 4;break;default :break;
}

对应汇编:

    51: switch (mode) 52: { 53:     case 0: 
0x0800018A 2D04      CMP      r5,#0x04
0x0800018C D017      BEQ      0x080001BE
0x0800018E DC07      BGT      0x080001A0
0x08000190 B16D      CBZ      r5,0x080001AE
0x08000192 2D01      CMP      r5,#0x01
0x08000194 D00D      BEQ      0x080001B2
0x08000196 2D02      CMP      r5,#0x02
0x08000198 D00D      BEQ      0x080001B6
0x0800019A 2D03      CMP      r5,#0x03
0x0800019C D117      BNE      0x080001CE
0x0800019E E00C      B        0x080001BA
0x080001A0 2D06      CMP      r5,#0x06
0x080001A2 D00E      BEQ      0x080001C2
0x080001A4 2D08      CMP      r5,#0x08
0x080001A6 D00E      BEQ      0x080001C6
0x080001A8 2D32      CMP      r5,#0x32
0x080001AA D110      BNE      0x080001CE
0x080001AC E00D      B        0x080001CA54:         count = 1; 
0x080001AE 2401      MOVS     r4,#0x0155:         break; 56:  57:     case 1: 
0x080001B0 E00E      B        0x080001D058:         count = 2; 
0x080001B2 2402      MOVS     r4,#0x0259:         break; 60:  61:     case 2: 
0x080001B4 E00C      B        0x080001D062:         count = 4; 
0x080001B6 2404      MOVS     r4,#0x0463:         break; 64:  65:     case 3: 
0x080001B8 E00A      B        0x080001D066:         count = 4; 
0x080001BA 2404      MOVS     r4,#0x0467:         break; 68:  69:     case 4: 
0x080001BC E008      B        0x080001D070:         count = 1; 
0x080001BE 2401      MOVS     r4,#0x0171:         break; 72:  73:     case 6: 
0x080001C0 E006      B        0x080001D074:         count = 2; 
0x080001C2 2402      MOVS     r4,#0x0275:         break; 76:  77:     case 8: 
0x080001C4 E004      B        0x080001D078:         count = 4; 
0x080001C6 2404      MOVS     r4,#0x0479:         break; 80:  81:     case 50: 
0x080001C8 E002      B        0x080001D082:         count = 4; 
0x080001CA 2404      MOVS     r4,#0x0483:         break; 84:  85:     default : 
0x080001CC E000      B        0x080001D086:         break; 87: } 

我们看到这种情况下,编译器采用了和第一种举例的情况的实现方式,也就是使用类似 if-else if的方式。

至于为什么会这样,是编译器看到 case 的值相差太大,如果用第二种那种跳转表的方式,占用的空间会大大增大(类似数组,即使你只用了索引 026,起码你也得定义数组 arr[27],大大浪费别的25个数组值),聪明编译器又岂会不懂这个道理,所以选择了这种方式,就是在时间和空间中选择了节省大量空间。

总结

我们看到,switch 语句的具体实现其实是会根据不同的情况会有不同的实现。编译器真是个聪明的东西呢。

以前还注意到 switch 的实现竟然还有那么多的门道,要学的东西真多,自己动手去探究也更能够理解更深刻。


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部