码字,杂谈

浅谈if...else与switch的区别和效率问题

各种语言都有条件语句,基本上所有语言都有if...else,大部分语言也都有switch,他们长得很像,基本上都可以相互转化使用。那么这两种语句有什么区别呢?

if…else

首先说说if...else,与其变种if...else if...else,部分语言可能长得不太一样,但是大同小异。

我们通常是这样使用的:

int a = 2;

if (a == 1) {
    cout << "a = 1" << endl;
}
else if (a == 2) {
    cout << "a = 2" << endl;
}
else {
    cout << "other number" << endl;
}

这个很好理解,它会从上至下依次进行判断,当有一个条件满足时,进入其中执行代码,执行完成后跳出整个条件语句。

这样的话,当条件很多时,比如10个,100个,它需要从上至下一条一条进行判断,当恰好最后一个条件命中满足时,这是多么恐怖的事情。

所以,使用if...else语句时需要注意,我们需要把命中率高的条件放在最前面。

switch

相比之下,有时候可能我们更喜欢switch语句的格式整齐,当然,也有很多公司对使用switch有很多要求甚至禁用,这都是语法规范层面,我们来看看效率。

通常,我们是这样使用switch的:

int a = 2;

switch (a)
{
case 1:
    cout << "a = 1" << endl;
    break;
case 2:
    cout << "a = 2" << endl;
    break;
default:
    cout << "other number" << endl;
    break;
}

看上去确实和if...else差不多,他们确实也可以互相转化。那么如何计算他们的执行逻辑呢?我们看一下switch的汇编:

    switch (a)
00AC5E8F 8B 45 F8             mov         eax,dword ptr [a]  
00AC5E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00AC5E98 83 BD 30 FF FF FF 01 cmp         dword ptr [ebp-0D0h],1  
00AC5E9F 74 0B                je          main+4Ch (0AC5EACh)  
00AC5EA1 83 BD 30 FF FF FF 02 cmp         dword ptr [ebp-0D0h],2  
00AC5EA8 74 2D                je          main+77h (0AC5ED7h)  
00AC5EAA EB 56                jmp         main+0A2h (0AC5F02h)

很明显,这段和if...else差不多,同样是比较(cmp)了两次,如果相等跳转(je)到指定内存,如果不等,执行最后一条无条件跳转(jmp)跳出当前条件语句。

我们发现这个和if...else还真是差不多。那么多一些条件:

switch条件数较多情况

int a = 2;

switch (a)
{
case 1: 
    cout << "a = 1" << endl;
    break;
case 2:
    cout << "a = 2" << endl;
    break;
case 3:
    cout << "a = 3" << endl;
    break;
case 4: 
    cout << "a = 4" << endl;
    break;
case 5: 
    cout << "a = 5" << endl;
    break;
case 6:
    cout << "a = 6" << endl;
    break;
default:
    cout << "other number" << endl;
    break;
}

再来看一下这段代码的汇编:

    switch (a)
00C45E8F 8B 45 F8             mov         eax,dword ptr [a]  
00C45E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00C45E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00C45E9E 83 E9 01             sub         ecx,1  
00C45EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00C45EA7 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5  
00C45EAE 0F 87 18 01 00 00    ja          $LN9+2Bh (0C45FCCh)  
00C45EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00C45EBA FF 24 95 0C 60 C4 00 jmp         dword ptr [edx*4+0C4600Ch]  

啊哈,我们发现变化很大,只有一个比较(cmp)了,也只有一个小于跳转(ja),同时多了一个减法(sub)操作。

这是因为我们的编译器很聪明,它能在编译时发现不同情况(下面还有更多情况)。当编译器看到switch语句时,首先进行一大段操作,之后就是不同条件下的操作,可以不用关心。

在这种条件较多的情况下,编译器首先会将所有条件排序,减去最小条件值(这里是1),然后比较条件最大值与条件最小值的差值(这里是5),如果比这个数大,则直接跳转到default语句,否则执行最后一句的jmp dword ptr [edx*4+0C4600Ch],我们发现每次它都会乘4,这是因为int在x86架构中占4字节。也就是说,从内存的0C4600Ch位置开始,存放了条件1的操作地址,依次往后为条件2、条件3…的操作地址。

这样的话,就能完美执行1-6的各种条件。

这样有几种比较特殊的情况:

起始条件如果不为1

根据上面思路,减去相应的起始数字即可。比如起始为2,则会每次减去2。

    switch (a)
00615E8F 8B 45 F8             mov         eax,dword ptr [a]  
00615E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00615E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00615E9E 83 E9 02             sub         ecx,2  
00615EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00615EA7 83 BD 30 FF FF FF 04 cmp         dword ptr [ebp-0D0h],4  
00615EAE 0F 87 EA 00 00 00    ja          $LN8+2Bh (0615F9Eh)  
00615EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00615EBA FF 24 95 E0 5F 61 00 jmp         dword ptr [edx*4+615FE0h]

很明显我们看到减法操作(sub)中每次都会减去2,这是因为我们起始值为2的缘故,当然,比较的数字也从5变成了4,因为我们最大的条件为6,6-2为4。

中间不连续

比如我们判断a = 3,并查找a,在条件2和条件4之间没有条件3,汇编为:

    switch (a)
00C75E8F 8B 45 F8             mov         eax,dword ptr [a]  
00C75E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00C75E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00C75E9E 83 E9 01             sub         ecx,1  
00C75EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00C75EA7 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5  
00C75EAE 0F 87 EA 00 00 00    ja          $LN8+2Bh (0C75F9Eh)  
00C75EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00C75EBA FF 24 95 E0 5F C7 00 jmp         dword ptr [edx*4+0C75FE0h] 

这时编译器也很聪明,为了保持上面的操作,它会在条件3的地址中保存default语句的操作地址,编译器也可以通过最后一句jmp dword ptr [edx*4+0C75FE0h]直接找到改地址,并继续执行default对应的操作。

对应的条件3的地址取出来为:0x00C75FE8 9e 5f c7 00,对应的地址就是:00c75f9e,操作的汇编为:

    default:
        cout << "other number" << endl;
00C75F9E 8B F4                mov         esi,esp  
00C75FA0 68 A3 12 C7 00       push        offset std::endl<char,std::char_traits<char> > (0C712A3h)  
00C75FA5 68 04 9C C7 00       push        offset string "a = 10" (0C79C04h)  
00C75FAA A1 D4 D0 C7 00       mov         eax,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0C7D0D4h)]  
00C75FAF 50                   push        eax  
00C75FB0 E8 53 B2 FF FF       call        std::operator<<<std::char_traits<char> > (0C71208h)  
00C75FB5 83 C4 08             add         esp,8  
00C75FB8 8B C8                mov         ecx,eax  
00C75FBA FF 15 A0 D0 C7 00    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0C7D0A0h)]  
00C75FC0 3B F4                cmp         esi,esp  
00C75FC2 E8 B9 B2 FF FF       call        __RTC_CheckEsp (0C71280h) 

我们看到从计算出来的地址可以直接跳转到default操作。

上述是特殊情况中比较普通的,还有几种更为特殊的,编译器也会更加聪明:

条件的差值较大

比如,我们有这样的代码:

int a = 2;

switch (a)
{
case 1: 
    cout << "a = 1" << endl;
    break;
case 2:
    cout << "a = 2" << endl;
    break;
case 3:
    cout << "a = 3" << endl;
    break;
case 4: 
    cout << "a = 4" << endl;
    break;
case 5: 
    cout << "a = 5" << endl;
    break;
case 6:
    cout << "a = 6" << endl;
    break;
case 100:
    cout << "a = 100" << endl;
    break;
default:
    cout << "other number" << endl;
    break;
}

我们再来看一下汇编:

    switch (a)
00335E8F 8B 45 F8             mov         eax,dword ptr [a]  
00335E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00335E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00335E9E 83 E9 01             sub         ecx,1  
00335EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00335EA7 83 BD 30 FF FF FF 63 cmp         dword ptr [ebp-0D0h],63h  
00335EAE 0F 87 4D 01 00 00    ja          $LN10+2Bh (0336001h)  
00335EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00335EBA 0F B6 82 60 60 33 00 movzx       eax,byte ptr [edx+336060h]  
00335EC1 FF 24 85 40 60 33 00 jmp         dword ptr [eax*4+336040h] 

发现好像跟之前差不多,依旧是计算地址,但是不同的是在跳转之前多了一条movzx eax,byte ptr [edx+336060h],那我们就查看一下336060长什么样子:

0x00336060  00 01 02 03  ....
0x00336064  04 05 07 07  ....
0x00336068  07 07 07 07  ....
0x0033606C  07 07 07 07  ....
0x00336070  07 07 07 07  ....
0x00336074  07 07 07 07  ....
0x00336078  07 07 07 07  ....
0x0033607C  07 07 07 07  ....
0x00336080  07 07 07 07  ....
0x00336084  07 07 07 07  ....
0x00336088  07 07 07 07  ....
0x0033608C  07 07 07 07  ....
0x00336090  07 07 07 07  ....
0x00336094  07 07 07 07  ....
0x00336098  07 07 07 07  ....
0x0033609C  07 07 07 07  ....
0x003360A0  07 07 07 07  ....
0x003360A4  07 07 07 07  ....
0x003360A8  07 07 07 07  ....
0x003360AC  07 07 07 07  ....
0x003360B0  07 07 07 07  ....
0x003360B4  07 07 07 07  ....
0x003360B8  07 07 07 07  ....
0x003360BC  07 07 07 07  ....
0x003360C0  07 07 07 06  ....

有没有很眼熟,这不就是单字节的数组么?那么这些数字也可以很好理解,这就是对应的不同操作,猜也能猜到,07表示default操作,之前从00-06对应相应的操作,取出这些数字后在进行dword ptr [eax*4+336040h]操作,则得到操作地址。

数字差值很大很大

刚才差值是100左右,编译器感觉不够大,我们来个更大的:

int a = 2;

switch (a)
{
case 1: 
    cout << "a = 1" << endl;
    break;
case 2:
    cout << "a = 2" << endl;
    break;
case 3:
    cout << "a = 3" << endl;
    break;
case 4: 
    cout << "a = 4" << endl;
    break;
case 5: 
    cout << "a = 5" << endl;
    break;
case 6:
    cout << "a = 6" << endl;
    break;
case 10000:
    cout << "a = 10000" << endl;
    break;
default:
    cout << "other number" << endl;
    break;
}

这次比较10000,再来看一下汇编的变化:

    switch (a)
006D5E8F 8B 45 F8             mov         eax,dword ptr [a]  
006D5E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
006D5E98 81 BD 30 FF FF FF 10 27 00 00 cmp         dword ptr [ebp-0D0h],2710h  
006D5EA2 7F 39                jg          main+7Dh (06D5EDDh)  
006D5EA4 81 BD 30 FF FF FF 10 27 00 00 cmp         dword ptr [ebp-0D0h],2710h  
006D5EAE 0F 84 3C 01 00 00    je          $LN9+2Bh (06D5FF0h)  
006D5EB4 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
006D5EBA 83 E9 01             sub         ecx,1  
006D5EBD 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
006D5EC3 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5  
006D5ECA 0F 87 4B 01 00 00    ja          $LN9+56h (06D601Bh)  
006D5ED0 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
006D5ED6 FF 24 95 5C 60 6D 00 jmp         dword ptr [edx*4+6D605Ch]  
006D5EDD E9 39 01 00 00       jmp         $LN9+56h (06D601Bh)  

这次变化就很大了,编译器不会允许创建一个10000字节的连续内存,它会首先跟最大值进行比较,然后有条件跳转到较小的分段空间中,最后根据条件找到对应的操作地址。

总结

相比if...elseswitch其根本原理还是通过数字直接跳转,不会依次比较,这在条件规模比较大且比较连续的情况下,switch效率会明显高于if...else,这也是为什么switch只允许传入整数和字符的原因。

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注