更深入一点理解 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>>

|