switch case顺序的深入探讨

来源:互联网 发布:scartch 2.0 mac下载 编辑:程序博客网 时间:2024/05/17 08:09

标     题:   更深入一点理解switch语句及c/c++对const的处理  
发信站:   BBS   水木清华站   (Thu   Feb   24   20:58:16   2005),   站内  
 
更深入一点理解   switch   语句   及   c/c++   对   const   的处理  
                                                                        谢煜波  
 
 
前段时间在论坛上看见台湾李维在 < <Borland传奇> > 一书中对windows编程模式中,消息处理部分有如下的一些分析:  
他说,在消息处理循环中,一般的形式是这样的  
MSG   msg   ;  
switch(   msg   ){  
                case   WM_XXXXXXX   :  
                                ....  
                case   WM_XXXXXXX   :  
                                ....  
                case   WM_XXXXXXX   :  
                                ....  
}   ;  
李维说,这种模式是很低效的,因应经过汇编后,这种C代码会产生如下的汇编代码  
                cmp   ....   .....  
                jnz   ....   .....  
                cmp   ....   .....  
                jnz   ....   .....  
                cmp   ....   .....  
                jnz   ....   .....  
如果你的   case   足够多,比如,你有一万条消息需要处理,而不幸的是你把一条最常用的消息  
放在了最后一位,那么当这条消息要得到处理,会首先经过一万次的cmp与jnz,   李维认为,这  
是非常非常低效的,实在是低效的忍无可忍,无需再忍~~:P  
 
在起初,我也是这样认为的,但近来的阅读及实验却发现,这种看法非常片面,今天就来谈谈这个问题(   所有实验在   linux   平台下完成   )  
 
首先看一到用   c   编写的程序  
/*   --------------------   filename   :   ta.c   ---------------   */  
int   switch_test_first(   int   x   )  
{  
                int   res   ;  
                switch(   x   ){  
                                case   100   :  
                                                res   =   1   ;  
                                                break   ;  
                                case   102   :  
                                                res   =   2   ;  
                                                break   ;  
                                case   103   :  
                                                res   =   3   ;  
                                                break   ;  
                }  
                return   res   ;  
}  
然后,我们用   gcc   将它编译成汇编文件(   使用   -S   开关   )  
gcc   -S   ta.c    
将得到如下的汇编文件(   ta.s   )  
                .file       "ta.c "  
                .text  
.globl   switch_test_first  
                .type       switch_test_first,@function  
switch_test_first:  
                pushl       %ebp  
                movl         %esp,   %ebp  
                subl         $8,   %esp  
                movl         8(%ebp),   %eax  
                .file       "ta.c "  
                .text  
.globl   switch_test_first  
                .type       switch_test_first,@function  
switch_test_first:  
                pushl       %ebp  
                movl         %esp,   %ebp  
                subl         $8,   %esp  
                movl         8(%ebp),   %eax  
                movl         %eax,   -8(%ebp)  
                cmpl         $102,   -8(%ebp)                     //   1  
                je             .L4                                           //   2  
                cmpl         $102,   -8(%ebp)                     //   3          
                jg             .L8                                           //   4  
                cmpl         $100,   -8(%ebp)                     //   5  
                je             .L3                                           //   6  
                jmp           .L2                                           //   7  
.L8:  
                cmpl         $103,   -8(%ebp)  
                je             .L5  
                jmp           .L2  
.L3:  
                movl         $1,   -4(%ebp)  
                jmp           .L2  
.L4:  
                movl         $2,   -4(%ebp)  
                jmp           .L2  
.L5:  
                movl         $3,   -4(%ebp)  
.L2:  
                movl         -4(%ebp),   %eax  
                leave  
                ret  
.Lfe1:  
                .size       switch_test_first,.Lfe1-switch_test_first  
                .ident     "GCC:   (GNU)   3.2.2   20030222   (Red   Hat   Linux   3.2.2-5) "  
 
注意看文件中   //   1   ~   //   7   的部份,从这个部份,我们可以看出,gcc确实是把一些case语句转成了李维所说的那种方式进行处理,我们看见了代码中存在有众多的   cmpl   与   jmp   语句  
这就相当于你使用if..else..一样,但是否总是这样呢?  
 
我们下面改动一下   ta.c   这个文件,在里面再多加一些   case   语句  
/*   --------------   filename   :   new_ta.c   -------------------   */  
int   switch_test_first(   int   x   )  
{  
                int   res   ;  
                switch(   x   ){  
                                case   100   :  
                                                res   =   1   ;  
                                                break   ;  
                                case   102   :  
                                                res   =   2   ;  
                                                break   ;  
                                case   103   :  
                                                res   =   3   ;  
                                                break   ;  
                                case   104   :  
                                                res   =   4   ;  
                                                break   ;  
                                case   105   :  
                                                res   =   5   ;  
                                                break   ;  
                                case   106   :  
                                                res   =   6   ;  
                                                break   ;  
                }  
                return   res   ;  
}  
这个   new_ta.c   与原来的   ta.c   在结构上完全相同,唯一不同的就是   case   语句的数量变多了,下面我们来编译一下这个文件  
gcc   -S   new_ta.c    
下面是我们产生的更新的汇编文件  
                .file       "new_ta.c "  
                .text  
.globl   switch_test_first  
                .type       switch_test_first,@function  
switch_test_first:  
                pushl       %ebp  
                movl         %esp,   %ebp  
                subl         $8,   %esp  
                movl         8(%ebp),   %eax  
                subl         $100,   %eax  
                movl         %eax,   -8(%ebp)  
                cmpl         $6,   -8(%ebp)  
                ja             .L2  
                movl         -8(%ebp),   %edx  
                movl         .L9(,%edx,4),   %eax  
                jmp           *%eax  
                .section                 .rodata  
                .align   4  
                .align   4  
.L9:                                                           //   A  
                .long       .L3  
                .long       .L2  
                .long       .L4  
                .long       .L5  
                .long       .L6  
                .long       .L7  
                .long       .L8  
                .text  
.L3:                                                         //   1  
                movl         $1,   -4(%ebp)  
                jmp           .L2  
.L4:                                                         //   2    
                movl         $2,   -4(%ebp)  
                jmp           .L2  
.L5:                                                         //   3  
                movl         $3,   -4(%ebp)  
                jmp           .L2                           //   4        
.L6:  
                movl         $4,   -4(%ebp)  
                jmp           .L2                           //   5  
.L7:  
                movl         $5,   -4(%ebp)         //   6  
                jmp           .L2  
.L8:                                                         //   7  
                movl         $6,   -4(%ebp)  
.L2:                                                          
                movl         -4(%ebp),   %eax  
                leave  
                ret  
.Lfe1:  
                .size       switch_test_first,.Lfe1-switch_test_first  
                .ident     "GCC:   (GNU)   3.2.2   20030222   (Red   Hat   Linux   3.2.2-5) "  


仔细比较一下这个最新的   new_ta.s   与前面的   ta.s,精华全在里面了!  
首先   new_ta.s   比前面的   ta.s   多了一个   .L9   部分,而且它的   //   1   ~   //   7   中没有了前面  
ta.s   文件中所存在的众多的   cmpl   与   jmp   语句,那么,现在这样的代码又是怎么实现    
switch   语句中的跳转的呢?我们来仔细分析一下它新多出来的   .L9   部份。  
                .section                 .rodata  
                .align   4  
                .align   4  
.L9:  
                .long       .L3  
                .long       .L2  
                .long       .L4  
                .long       .L5  
                .long       .L6  
                .long       .L7  
                .long       .L8  
                .text  
显而易见,.L9   部份是一个我们最常见的数据结构——表,它的每一项都是一个标号,而这个标号,恰恰是每个   case   语句的入口标号!  
这很容易让我们想到,它很可能是用了一张表来存放所有的   case   语句的入口,然后,在  
执行   switch   语句的时候就从这个表中直接检出相应的   case   语句的入口地址,然后跳转  
到相应的   case   语句去执行,就像hash_table似的。具体是不是这样呢?我们看看进入  
switch   部份的代码:  
 
                pushl       %ebp  
                movl         %esp,   %ebp  
                subl         $8,   %esp  
                movl         8(%ebp),   %eax  
                subl         $100,   %eax  
                movl         %eax,   -8(%ebp)  
                cmpl         $6,   -8(%ebp)  
                ja             .L2  
                movl         -8(%ebp),   %edx  
                movl         .L9(,%edx,4),   %eax   //   1  
                jmp           *%eax                             //   2  
果然如此!首先在   //   1   处根据%edp的值(其值相当于表的下标)在.L9的表中找到相应  
case   语句的入口地址,并把这个地址存到%eax中,然后通过   //   2   (这是一个间接跳转  
语句)转到%eax存放的地址中,也即相应的case语句处。  
C编译器,果然聪明!  
 
通过这个分析我们可以知道如下两点:  
1.   当   case   语句少的时候,C编译器将其转成   if..else..   类型进行处理,运用较多的  
      cmp   与   jmp   语句   ,而当   case   语句较多的时候,C编译器会出成一个跳转表,而直  
      接通过跳转表进行跳转,这让   switch   具有非常高的效律,而且效律几乎不会因为  
      case   语句的增长而减小,李维所担忧的问题是完全不会发生的  
2.   可以问答下面几个问题:  
      1.   为什么   case   语句中需要的是整数类型而不能是其余的类型?  
            这是因为,case   语句中的这个值是用来做跳转表的下标的,因此,当然必须是整数  
      2.   为什么   case   语句在不加break的时候具有直通性?  
            这是因为跳转是在进入   switch   是计算出的,而不是在case语句中计算出的,整个  
            case   语句群就是一块完整而连续的代码,只是switch让其从不同的位置开始执行。  
 
上面的内容,在《Computer   Systems   A   Programmer 's   Perspective》中有很详细的论述,  
感兴趣可以去找来仔细看看~~~  
 
既然,case   语句需要的是整数的常量值,那么我们是否可用   const   类型呢?比如下面  
一段代码:  
 
const   int   c_1   =   100   ;  
const   int   c_2   =   102   ;  
 
void   test(   int   x   )  
{  
                switch(   x   ){  
                                case   c_1   :  
                                                ++x   ;  
                                case   c_2   :  
                                                --x   ;  
                }  
}  
 
这段代码,用   c   编译器编译,编译器会提示错误,但在   c++   编译器中却不会,这主要是由于   c   ,   与   c++   编译器对   const   这个东东的处理不同。我们来看看下面一段   c   程序  
/*-------------   filename   :   const_c.c   -----------*/  
const   int   a   =   15   ;  
 
void   f(   int   x   )  
{  
                x   =   a   ;  
}  
同样用   gcc   编译    
gcc   -S   const_c.c  
然后,来看看它的汇编文件  
                .file       "const_c.c "  
.globl   a  
                .section                 .rodata  
                .align   4  
                .type       a,@object  
                .size       a,4  
a:                                                           //   1  
                .long       15  
                .text  
.globl   f  
                .type       f,@function  
f:  
                pushl       %ebp  
                movl         %esp,   %ebp  
                movl         a,   %eax                 //   2      
                movl         %eax,   8(%ebp)  
                leave  
                ret  
.Lfe1:  
                .size       f,.Lfe1-f  
                .ident     "GCC:   (GNU)   3.2.2   20030222   (Red   Hat   Linux   3.2.2-5) "  
注意   //   1   处,C   编译器为   a   分配了地址,并把它的值设为   15   ,而在   //   2   处,它是将  
a   这个地址中的值赋给了   %eax,这同一般的普通变量而非const   变量赋值没什么两样  
 
下面我们用   c++   编译器来编译这段代码,它产生的汇编文件如下:  
                .file       "const_cpp.cpp "  
                .text  
                .align   2  
.globl   _Z1fi  
                .type       _Z1fi,@function  
_Z1fi:  
.LFB2:  
                pushl       %ebp  
.LCFI0:  
                movl         %esp,   %ebp  
.LCFI1:  
                movl         $15,   8(%ebp)     //   1  
                leave  
                ret  
.LFE2:  
.Lfe1:  
                .size       _Z1fi,.Lfe1-_Z1fi  
                .section                 .rodata  
                .align   4  
                .type       a,@object  
                .size       a,4  
a:  
                .long       15  
                .ident     "GCC:   (GNU)   3.2.2   20030222   (Red   Hat   Linux   3.2.2-5) "  
同样注意//   1   处,它以经把   a   的值用   15   来取代了,  
也就是说,在c中const变量的行为更像一个非const变量,而在cpp中,const变量的行为就像是#define  
由于   c++   中,const   变量的值是在编译时就计算出来的,因此,它可以用在   case   语句中,而   c   中,const值在编译时只是一个变量的地址,因此,它无法用在   case   语句中.  
-----------------------------------------------------------------------------  
参考文献: < <Computer   Systems   A   Programmer 's   Perspective> >  
转载请注明原作者,以出处~~    

原创粉丝点击