if else 与switch 效率&实现机制

我们先来看if else语句编译是怎么实现的

上代码

void main()
{
    int a=4;
    if (1==a)
    {
        printf("1");
    }
    else if(2==a)
    {
        printf("2");
    }
    else if(3==a)
    {
        printf("3");
    }
    else
    {
        printf("4");
    }
}

这里我们先试着三次判断,通过反汇编来看看编译器怎实现的

    int a=4;
004113EE  mov         dword ptr [a],4 //将4放入a中
    if (1==a)
004113F5  cmp         dword ptr [a],1 //1与4比较
004113F9  jne         main+44h (411414h) //相同为1则顺序执行下面,否则跳转到下一个if处
    {
        printf("1");
004113FB  mov         esi,esp 
004113FD  push        offset string "1" (415648h) 
00411402  call        dword ptr [__imp__printf (4182B8h)] 
00411408  add         esp,4 
0041140B  cmp         esi,esp 
0041140D  call        @ILT+305(__RTC_CheckEsp) (411136h) 
00411412  jmp         main+99h (411469h) 
    }
    else if(2==a)
00411414  cmp         dword ptr [a],2 //第一个if为假则这里执行比较
00411418  jne         main+63h (411433h) //比较为真顺序执行,为假跳到下一个else if
    {
        printf("2");
0041141A  mov         esi,esp 
0041141C  push        offset string "2" (415644h) 
00411421  call        dword ptr [__imp__printf (4182B8h)] 
00411427  add         esp,4 
0041142A  cmp         esi,esp       0041142C  call        @ILT+305(__RTC_CheckEsp) (411136h) 

00411431  jmp         main+99h (411469h) 
    }
    else if(3==a)
00411433  cmp         dword ptr [a],3 //上一个if为假这里开始比较
00411437  jne         main+82h (411452h) //比较为真顺序执行,为假跳转到最后一个else
    {
        printf("3");
00411439  mov         esi,esp 
0041143B  push        offset string "3" (415640h) 
00411440  call        dword ptr [__imp__printf (4182B8h)] 
00411446  add         esp,4 
00411449  cmp         esi,esp 
0041144B  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    }
    else
00411450  jmp         main+99h (411469h) //这个跳转指令是当上一个else if执行后顺序执行下来到这一条指令,意思是直接跳转到else语句}后面的语句
    {
        printf("4");
00411452  mov         esi,esp 
00411454  push        offset string "0" (41563Ch) 
00411459  call        dword ptr [__imp__printf (4182B8h)] 
0041145F  add         esp,4 
00411462  cmp         esi,esp 
00411464  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    }
}

这里通过查看反汇编可以看出if else 结构是顺序比较(除去最后一个else不比较),特别是当产生的分支结构较多时,效率很低。

那可能有人问那 if if if。。。结构是怎么的

int a=3;
004113EE  mov         dword ptr [a],3 

if (1==a)
004113F5  cmp         dword ptr [a],1 
004113F9  jne         main+42h (411412h) 
{
    printf("1");
004113FB  mov         esi,esp 
004113FD  push        offset string "1" (415648h) 
00411402  call        dword ptr [__imp__printf (4182B8h)] 
00411408  add         esp,4 
0041140B  cmp         esi,esp 
0041140D  call        @ILT+305(__RTC_CheckEsp) (411136h) 
}
if(2==a)
00411412  cmp         dword ptr [a],2 
00411416  jne         main+5Fh (41142Fh) 
{
    printf("2");
00411418  mov         esi,esp 
0041141A  push        offset string "2" (415644h) 
0041141F  call        dword ptr [__imp__printf (4182B8h)] 
00411425  add         esp,4 
00411428  cmp         esi,esp 
0041142A  call        @ILT+305(__RTC_CheckEsp) (411136h) 
}
 if(3==a)
0041142F  cmp         dword ptr [a],3 
00411433  jne         main+7Ch (41144Ch) 
{
    printf("3");
00411435  mov         esi,esp 
00411437  push        offset string "3" (415640h) 
0041143C  call        dword ptr [__imp__printf (4182B8h)] 
00411442  add         esp,4 
00411445  cmp         esi,esp 
00411447  call        @ILT+305(__RTC_CheckEsp) (411136h) 
}
if (4==a)
0041144C  cmp         dword ptr [a],4 
00411450  jne         main+99h (411469h) 
{
    printf("4");
00411452  mov         esi,esp 
00411454  push        offset string "0" (41563Ch) 
00411459  call        dword ptr [__imp__printf (4182B8h)] 
0041145F  add         esp,4 
00411462  cmp         esi,esp 
00411464  call        @ILT+305(__RTC_CheckEsp) (411136h) 
}

这里我们将每一个else去掉,都写成if结构,通过反汇编不难看出,差距不大只是最后一个if多比较了一次,但是依旧是多分支结构时耗损多。

那我们再看看switch case结构又有什么优点?

上代码

int a=3;
switch(a)
{
case 1:
    printf("1");
    break;
case 2:
    printf("2");
    break;
case 3:
    printf("3");
    break;
default:
    printf("0");
}

这里先选择一下三个case语句,注意这里是switch case 的一个重点

反汇编

int a=3;
004113EE  mov         dword ptr [a],3 

```
switch(a)
004113F5  mov         eax,dword ptr [a] 
004113F8  mov         dword ptr [ebp-0DCh],eax 
004113FE  cmp         dword ptr [ebp-0DCh],1 
00411405  je          main+4Bh (41141Bh) 
00411407  cmp         dword ptr [ebp-0DCh],2 
0041140E  je          main+64h (411434h) 
00411410  cmp         dword ptr [ebp-0DCh],3 
00411417  je          main+7Dh (41144Dh) 
00411419  jmp         main+96h (411466h) 
```
{
case 1:
    printf("1");
0041141B  mov         esi,esp 
0041141D  push        offset string "1" (415648h) 
00411422  call        dword ptr [__imp__printf (4182B8h)] 
00411428  add         esp,4 
0041142B  cmp         esi,esp 
0041142D  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    break;
00411432  jmp         main+0ADh (41147Dh) 
case 2:
    printf("2");
00411434  mov         esi,esp 
00411436  push        offset string "2" (415644h) 
0041143B  call        dword ptr [__imp__printf (4182B8h)] 
00411441  add         esp,4 
00411444  cmp         esi,esp 
00411446  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    break;
0041144B  jmp         main+0ADh (41147Dh) 
case 3:
    printf("3");
0041144D  mov         esi,esp 
0041144F  push        offset string "3" (415640h) 
00411454  call        dword ptr [__imp__printf (4182B8h)] 
0041145A  add         esp,4 
0041145D  cmp         esi,esp 
0041145F  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    break;
00411464  jmp         main+0ADh (41147Dh) 
default:
    printf("0");
00411466  mov         esi,esp 
00411468  push        offset string "0" (41563Ch) 
0041146D  call        dword ptr [__imp__printf (4182B8h)] 
00411473  add         esp,4 
00411476  cmp         esi,esp 
00411478  call        @ILT+305(__RTC_CheckEsp) (411136h) 
}

通过反汇编我们查看到,编译器对于分支在3或3一下的switch case 的做法与if else 的做法,大径相同都是采用局部变量与比较值进行比较,依次要比较3次(最坏情况),根据比较结果是否顺序执行指令,或者直接跳转相应指令。

那么,当switch case语句的分支达到3个以上时,编译器还有没有这样做?

我们再加入一个分支,上反汇编

int a=3;
004113EE  mov         dword ptr [a],3 
```
switch(a)
004113F5  mov         eax,dword ptr [a] 
004113F8  mov         dword ptr [ebp-0DCh],eax 
004113FE  mov         ecx,dword ptr [ebp-0DCh] 
00411404  sub         ecx,1 
00411407  mov         dword ptr [ebp-0DCh],ecx 
0041140D  cmp         dword ptr [ebp-0DCh],3 
00411414  ja          $LN2+19h (411487h) 
00411416  mov         edx,dword ptr [ebp-0DCh] 
0041141C  jmp         dword ptr  (4114BCh)[edx*4] 
```
{
case 1:
    printf("1");
00411423  mov         esi,esp 
00411425  push        offset string "1" (415660h) 
0041142A  call        dword ptr [__imp__printf (4182B8h)] 
00411430  add         esp,4 
00411433  cmp         esi,esp 
00411435  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    break;
0041143A  jmp         $LN2+30h (41149Eh) 
case 2:
    printf("2");
0041143C  mov         esi,esp 
0041143E  push        offset string "2" (415648h) 
00411443  call        dword ptr [__imp__printf (4182B8h)] 
00411449  add         esp,4 
0041144C  cmp         esi,esp 
0041144E  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    break;
00411453  jmp         $LN2+30h (41149Eh) 
case 3:
    printf("3");
00411455  mov         esi,esp 
00411457  push        offset string "3" (415644h) 
    0041145C  call        dword ptr [__imp__printf (4182B8h)] 
00411462  add         esp,4 
00411465  cmp         esi,esp 
00411467  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    break;
0041146C  jmp         $LN2+30h (41149Eh) 
case 4:
    printf("4");
0041146E  mov         esi,esp 
00411470  push        offset string "4" (415640h) 
00411475  call        dword ptr [__imp__printf (4182B8h)] 
0041147B  add         esp,4 
0041147E  cmp         esi,esp 
00411480  call        @ILT+305(__RTC_CheckEsp) (411136h) 
    break;
00411485  jmp         $LN2+30h (41149Eh) 
default:
    printf("0");
00411487  mov         esi,esp 
00411489  push        offset string "0" (41563Ch) 
0041148E  call        dword ptr [__imp__printf (4182B8h)] 
00411494  add         esp,4 
00411497  cmp         esi,esp 
00411499  call        @ILT+305(__RTC_CheckEsp) (411136h) 
}

很明显我们发现switch下面的指令不一样的了,这里我们来看看这些指令到底干了什么!

int a=3;
004113EE  mov         dword ptr [a],3 
switch(a)
004113F5  mov         eax,dword ptr [a] 
//将a的值给了eax,eax存放a
004113F8  mov         dword ptr [ebp-0DCh],eax 
//将eax的值存放入一个局部变量[ebp-0DCh],局部变量值为a
004113FE  mov         ecx,dword ptr [ebp-0DCh] 
//再将局部变量的值给ecx,ecx的值为a
00411404  sub         ecx,1 
//将ecx的值减1,ecx=a-1
00411407  mov         dword ptr [ebp-0DCh],ecx 
//将减后ecx的值给刚才的局部变量,局部变量值为a-1
0041140D  cmp         dword ptr [ebp-0DCh],3 
//让局部变量的值与3进行比较
00411414  ja          $LN2+19h (411487h) 
//如果 局部变量值 > 3 则跳转到输出default
00411416  mov         edx,dword ptr [ebp-0DCh] 
//将局部变量的值给edx,edx为a-1
0041141C  jmp         dword ptr  (4114BCh)[edx*4] 
//跳转到跳转表所指定的地址

额,看完解释可能还是不懂,这个逻辑到底是怎么样的。
这样我们用逻辑性的语言来解释

首先编译器将switch case语句的case后面那个常量做成了一个表,
由于switch case 中只能用整型常量来做判断,所以每一个表中的元素都是4字节,这4个字节里存放的都是对应case的语句的下面执行指令的地址,就是EIP的值。
我的表建立在(4114BCh)这里,通过内存查看到按照顺序来看
0x004114BC  23 14 41 00  #.A.//第一个case下面的指令00411423
0x004114C0  3c 14 41 00  <.A.//第二个case下面的指令0041143c
0x004114C4  55 14 41 00  U.A.//第三个case下面的指令00411455
0x004114C8  6e 14 41 00  n.A.//第四个case下面的指令0041146e

那么表中建立的是指令地址,那么常量是怎么用呢?

我们向switch中给了一个整型变量,这里我们将整型变量的值-1与最大一个case 后面的整型常量值-1,进行比较,如果大于则说明没有case符合跳到default来执行。

那为啥要减1?

0041141C  jmp         dword ptr  (4114BCh)[edx*4] 

edx就是偏移量,这样由于我是定义四个case语句,如果不给传入的整型变量-1
那么edx就是3,3*4是12。
第一个case 的指令的偏移量是0
第二个case 的指令的偏移量是4
第三个case 的指令的偏移量是8
第四个case 的指令的偏移量是12

咋一看,没问题,但是符合我们整型变量的case是第三个,这一弄导致出错了,所有我们采用-1的这个方法。

可能有人问了,那我如果是case -1,或者case -2 或者case 后面的值完全乱序,一会正一会负,那又是如何实现!!

如果有人做了,那就会发现我上面总结的都是狗屁不通,强行解释。。。(= ̄ω ̄=)

现在我们来看看乱了后到底是怎么样,当然这些变化主要体现在这几个指令上

sub指令是会变成add还是消失了?
cmp指令后面比较的值到底是根据什么变化的?
跳转表又是怎么样了?怎么弄的?

第一

乱序:当case后只有正数时,没有sub指令也没有add而是消失了
cmp指令后面是case中 正数最大值
jmp指令前增加一个指令

004135DD  movzx       edx,byte ptr  (413698h)[ecx] 
首先ecx是我们传入的整型变量值,edx值自然是case对应在表中的偏移量
换句换说,表中的指令地址按照了正数大侠进行排序存放地址。
例如:
    case 1
    case 12
    case 4
    case 24
    但在表中存放的是 1,4,12,24有序的case 下面指令的地址

第二

乱序 :当case 后面既有负数也有正数时,
sub指令变成了add指令,而加的值就是最小的负数
cmp指令后面的比较值则变成了 最小负数的绝对值+最大正数
jmp指令前还加了这个指令

004135DD  movzx       edx,byte ptr  (413698h)[ecx] 

当然这还没完,假设我们case后取很大的int值又会怎样?

第三

乱序:

是的你没看错

switch语句放弃了跳转表,开始直接比较。

当然想想还都是,过大还是要放弃的,当然这也有可能是我的分支太少。

更多分支可能会有其他变化!

当然还有 有序但数值上不连续的,这里我不讨论了。。。(= ̄ω ̄=)

第四

事实上,switch case有三种实现机制,分别是
    1、直接判断
    2、跳转表
    3、树型结构
编译器根据需要判断表达式的复杂性并采用相应的的实现方式。

总结

    1、switch case 与 if else 在常量比较上 当分支大于3时switch语句效率要高于if else语句

    2、当非常量比较时,如表达式的比较,自然是选择if else 结构了!

    3、为什么switch 要用int常量,看了跳转表就知道,如果是浮点数那偏移量不就完蛋了,怎么跳??

    4、还有使用switch语句时还是要 有序的 尽量从0开始,这样使得效率能更好

    5、从0开始自然就没sub那条指令咯!

初学,如有错误,请指出,我会修改!!!(= ̄ω ̄=)

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值