更深入一點理解 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>>
轉載請註明原作者,以出處~~
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章