华亮's profile灌吧PhotosBlogListsMore Tools Help
    12/15/2008

    [转] 更深入一点理解 switch 语句 及 c/c++ 对 const 的处理

    更深入一点理解 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>>
    转载请注明原作者,以出处~~

    Comments

    Please wait...
    Sorry, the comment you entered is too long. Please shorten it.
    You didn't enter anything. Please try again.
    Sorry, we can't add your comment right now. Please try again later.
    To add a comment, you need permission from your parent. Ask for permission
    Your parent has turned off comments.
    Sorry, we can't delete your comment right now. Please try again later.
    You've exceeded the maximum number of comments that can be left in one day. Please try again in 24 hours.
    Your account has had the ability to leave comments disabled because our systems indicate that you may be spamming other users. If you believe that your account has been disabled in error please contact Windows Live support.
    Complete the security check below to finish leaving your comment.
    The characters you type in the security check must match the characters in the picture or audio.

    To add a comment, sign in with your Windows Live ID (if you use Hotmail, Messenger, or Xbox LIVE, you have a Windows Live ID). Sign in


    Don't have a Windows Live ID? Sign up

    Trackbacks

    The trackback URL for this entry is:
    http://guanhl.spaces.live.com/blog/cns!12868090E475E79D!430.trak
    Weblogs that reference this entry
    • None