知識擴展–if…else…與switch…case…的執行原理if和switch的原理

  • 2019 年 10 月 8 日
  • 筆記

一、簡述

  程式語言中的條件分支結構有兩種:if-else和switch-case,這兩種條件分支之間可以相互轉換,但是也存在一些區別,那麼什麼時候該用if-else,什麼時候該用switch-case呢?這就需要我們去了解它們之間的練習和區別了。

1.1 if…else…簡述

if-else的基本知識點包含4點:

  • 單獨if語句:單分支結構,簡單的一個控制語句,如果滿足條件則做對應的操作,否則不做。
if( 條件 ) {    條件成立時執行的程式碼  }
  • if-else語句:雙分支結構,這兩個分支場景一定是相互對立,非此即彼的兩種場景。
if( 條件 ) {    條件成立時執行的程式碼  } else {    條件失敗時執行的程式碼  }
  • if-else-if語句:多分支結構,這多個分支最多只會執行一個分支的操作,而且執行過程是從上到下依次判斷,一旦某個條件滿足,執行對應的操作後就不會繼續執行後面的條件判斷了。
if ( 條件1 ) {    程式碼塊1  } else if ( 條件2 ) {      //在條件 1 不滿足的情況下,才會進行條件 2 的判斷    程式碼塊2  } else if ( 條件3 ) {    程式碼塊3  } else {      //當前面的條件均不成立時,才會執行 else 塊內的程式碼。    程式碼塊N  }
  • if嵌套:每一對大括弧對應的語句塊中都可以進行任何流程式控制制,所以任何的if語句塊、else語句塊中都可以繼續進行if-else的分支結構。但是只有當外層 if 的條件成立時,才會判斷內層 if 的條件。
//只有當外層 if 的條件成立時,才會判斷內層 if 的條件。  if(條件1){    if(條件2){      程式碼塊1    }else{      程式碼塊2    }  }else{    程式碼塊3  }

1.2 switch…case…簡述

  switch語句的基本結構如下,執行過程是當 switch 後表達式的值和 case 語句後的值相同時,從該位置開始向下執行,直到遇到 break 語句或者 switch 語句塊結束;如果沒有匹配的 case 語句則執行 default 塊的程式碼。

switch ( 常量表達式 ){      case 常量1 :語句;      case 常量2 :語句;      case 常量3 :語句;          ...      case 常量n:語句;      default :語句;  }
  • switch後面小括弧中常量表達式的值必須是整型或字元型(不同的程式語言規定不一樣,Java除了整數之外還可以是枚舉和字元串,PHP還可以是浮點數)
  • case後面的值可以是常量數值,如1、2;也可以是一個常量表達式,如2+2;但不能是變數或帶有變數的表達式,如a*2。
  • break的用法:case匹配後,執行匹配塊里的程式程式碼,如果沒有遇見break會繼續執行下一個case塊的內容,直到遇到break語句或這switch語句塊結束

二、執行原理分析

  這裡轉載自文章:if和switch的原理

  感興趣的同學還可以查看一下:If-else 三目運算符 底層實現 效率差異

2.1 if…else…執行原理

  在程式語言中,不管是那種程式語言,if和switch都是是條件分支的重要組成部分。if的功能是計算判斷條件的值,根據返回的值的不同來決定跳轉到哪個部分。值為真則跳轉到if語句塊中,否則跳過if語句塊。下面來分析一個簡單的if實例:

if(argc > 0) {      printf("argc > 0n");  }    if (argc <= 0) {      printf("argc <= 0n");  }  printf("argc = %dn", argc);
//上述程式碼對應的彙編程式碼如下  9:        if(argc > 0)    cmp         dword ptr [ebp+8],0  0040102C   jle         main+2Bh (0040103b) ;argc <= 0就跳轉到下一個if處  10:       {  11:           printf("argc > 0n");  0040102E   push        offset string "argc > 0n" (0042003c)    call        printf (00401090)    add         esp,4  12:       }  13:       if (argc <= 0) ;argc > 0跳轉到後面的printf語句輸出argc的值  0040103B   cmp         dword ptr [ebp+8],0  0040103F   jg          main+3Eh (0040104e)  14:       {  15:           printf("argc <= 0n");    push        offset string "argc <= 0n" (0042002c)    call        printf (00401090)  0040104B   add         esp,4  16:       }  17:       printf("argc = %dn", argc);  0040104E   mov         eax,dword ptr [ebp+8]    push        eax    push        offset string "argc = %dn" (0042001c)    call        printf (00401090)  0040105C   add         esp,8

  根據彙編程式碼我們看到,首先執行第一個if中的比較,jle表示當cmp得到的結果≤0時會進行跳轉,第二個if在彙編中的跳轉條件是>0,從這個上面可以看出在程式碼執行過程當中if轉換的條件判斷語句與if的判斷結果時相反的,也就是說cmp比較後不成立則跳轉,成立則向下執行。同時每一次跳轉都是到當前if語句的下一條語句

  下面來看看if…else…語句的跳轉:

if(argc > 0) {      printf("argc > 0n");  } else {      printf("argc <= 0n");  }  printf("argc = %dn", argc);
//上面程式碼對應的彙編程式碼  9:        if(argc > 0)      cmp         dword ptr [ebp+8],0  0040102C   jle         main+2Dh (0040103d) ;條件不滿足則跳轉到else語句塊中  10:       {  11:           printf("argc > 0n");  0040102E   push        offset string "argc > 0n" (0042003c)    call        printf (00401090)    add         esp,4  12:       }else  0040103B   jmp         main+3Ah (0040104a);如果執行if語句塊就會執行這條語句跳出else語句塊  13:       {  14:           printf("argc <= 0n");  0040103D   push        offset string "argc <= 0n" (0042002c)    call        printf (00401090)    add         esp,4  15:       }  16:       printf("argc = %dn", argc);  0040104A   mov         eax,dword ptr [ebp+8]

  上述的彙編程式碼指出,對於if…else..語句,首先進行條件判斷,if表達式為真,則繼續執行if快中的語句,然後利用jmp跳轉到else語句塊外,否則會利用jmp跳轉到else語句塊中,然後依次執行其後的每一句程式碼

  最後再來展示if…else if…else這種分支結構:

if(argc > 0) {      printf("argc > 0n");  } else if(argc < 0) {      printf("argc < 0n");  } else {      printf("argc == 0n");  }  printf("argc = %dn", argc);
//上述程式碼對應的彙編程式碼  9:        if(argc > 0)    cmp         dword ptr [ebp+8],0  0040102C   jle         main+2Dh (0040103d);條件不滿足則會跳轉到下一句else if中  10:       {  11:           printf("argc > 0n");  0040102E   push        offset string "argc > 0n" (00420f9c)    call        printf (00401090)    add         esp,4  12:       }else if(argc < 0)  0040103B   jmp         main+4Fh (0040105f)  ;當上述條件符合則執行這條語句跳出分支外,跳轉的地址正是else語句外的printf語句  0040103D   cmp         dword ptr [ebp+8],0    jge         main+42h (00401052)  13:       {  14:           printf("argc < 0n");    push        offset string "argc < 0n" (0042003c)    call        printf (00401090)  0040104D   add         esp,4  15:       }else    jmp         main+4Fh (0040105f)  16:       {  17:           printf("argc == 0n");    push        offset string "argc <= 0n" (0042002c)    call        printf (00401090)  0040105C   add         esp,4  18:       }  19:       printf("argc = %dn", argc);  0040105F   mov         eax,dword ptr [ebp+8]

  通過彙編程式碼可以看到對於這種結構,會依次判斷每個if語句中的條件,當有一個滿足,執行完對應語句塊中的程式碼後,會直接調轉到分支結構外部,當前面的條件都不滿足則會執行else語句塊中的內容。這個邏輯結構在某些情況下可以利用if return if return 這種結構來替代。當某一條件滿足時執行完對應的語句後直接返回而不執行其後的程式碼。一條提升效率的做法是將最有可能滿足的條件放在前面進行比較,這樣可以減少比較次數,提升效率

2.2 switch…case…執行原理

  switch是另一種比較常用的多分支結構,在使用上比較簡單,效率上也比if…else if…else高,下面將分析switch結構的實現:

switch(argc)  {    case 1:      printf("argc = 1n");      break;    case 2:      printf("argc = 2n");      break;     case 3:      printf("argc = 3n");      break;     case 4:      printf("argc = 4n");      break;     case 5:      printf("argc = 5n");      break;     case 6:      printf("argc = 6n");      break;     default:      printf("elsen");      break;  } 
//對應的彙編程式碼  0040B798   mov         eax,dword ptr [ebp+8] ;eax = argc  0040B79B   mov         dword ptr [ebp-4],eax  0040B79E   mov         ecx,dword ptr [ebp-4]  ;ecx = eax  0040B7A1   sub         ecx,1  0040B7A4   mov         dword ptr [ebp-4],ecx  0040B7A7   cmp         dword ptr [ebp-4],5 ;比較argc 與 5的大小關係  0040B7AB   ja          $L544+0Fh (0040b811) ;argc > 5則跳轉到default處,至於為什麼是5而不是6,看後面的說明  0040B7AD   mov         edx,dword ptr [ebp-4] ;edx = argc  0040B7B0   jmp         dword ptr [edx*4+40B831h]  11:       case 1:  12:           printf("argc = 1n");  0040B7B7   push        offset string "argc = 1n" (00420fc0)  0040B7BC   call        printf (00401090)  0040B7C1   add         esp,4  13:           break;  0040B7C4   jmp         $L544+1Ch (0040b81e)  14:       case 2:  15:           printf("argc = 2n");  0040B7C6   push        offset string "argc = 2n" (00420fb4)  0040B7CB   call        printf (00401090)  0040B7D0   add         esp,4  16:           break;  0040B7D3   jmp         $L544+1Ch (0040b81e)  17:       case 3:  18:           printf("argc = 3n");  0040B7D5   push        offset string "argc = 3n" (00420fa8)  0040B7DA   call        printf (00401090)  0040B7DF   add         esp,4  19:           break;  0040B7E2   jmp         $L544+1Ch (0040b81e)  20:       case 4:  21:           printf("argc = 4n");  0040B7E4   push        offset string "argc = 4n" (00420f9c)  0040B7E9   call        printf (00401090)  0040B7EE   add         esp,4  22:           break;  0040B7F1   jmp         $L544+1Ch (0040b81e)  23:       case 5:  24:           printf("argc = 5n");  0040B7F3   push        offset string "argc < 0n" (0042003c)  0040B7F8   call        printf (00401090)  0040B7FD   add         esp,4  25:           break;  0040B800   jmp         $L544+1Ch (0040b81e)  26:       case 6:  27:           printf("argc = 6n");  0040B802   push        offset string "argc <= 0n" (0042002c)  0040B807   call        printf (00401090)  0040B80C   add         esp,4  28:           break;  0040B80F   jmp         $L544+1Ch (0040b81e)  29:       default:  30:           printf("elsen");  0040B811   push        offset string "argc = %dn" (0042001c)  0040B816   call        printf (00401090)  0040B81B   add         esp,4  31:           break;  32:       }  33:  34:       return 0;  0040B81E   xor         eax,eax

  上面的程式碼中並沒有看到像if那樣,對每一個條件都進行比較,其中標紅的一行程式碼 「jmp dword ptr [edx*4+40B831h]」 從表面上看應該是取數組中的元素,再根據元素的值來進行跳轉,而這個元素在數組中的位置與eax也就是與argc的值有關,下面我們跟蹤到數組中查看數組的元素值:

0040B831    B7 B7 40 00          0040B835   C6 B7 40 00 0040B839   D5 B7 40 00 0040B83D   E4 B7 40 00 0040B841    F3 B7 40 00 0040B845   02 B8 40 00

  通過對比可以發現0x0040b7b7是case 1處的地址,後面的分別是case 2、case 3、case 4、case 5、case 6處的地址,每個case中的break語句都翻譯為了同一句話「jmp $L544+1Ch (0040b81e)」,所以從這可以看出,在switch中,編譯器多增加了一個數組用於存儲每個case對應的地址,根據switch中傳入的整數在數組中查到到對應的地址,直接通過這個地址跳轉到對應的位置,減少了比較操作,提升了效率。編譯器在處理switch時會首先校驗不滿足所有case的情況,當這種情況發生時程式碼調轉到default或者switch語句塊之外。然後將傳入的整數值減一(數組元素是從0開始計數)。最後根據參數值找到應該跳轉的位置

  上述的程式碼case是從0~6依次遞增,這樣做確實可行,但是當我們在case中的值並不是依次遞增的話會怎樣?此時根據不同的情況編譯器會做不同的處理。

  • 一般仍然會建立這樣的一個表,將case中出現的值填寫對應的跳轉地址,沒有出現的則將這個地址值填入default對應的地址或者switch語句結束的地址,比如當我們上述的程式碼去掉case 5, 這個時候填入的地址值如下圖所示
  • 如果每兩個case之間的差距大於6,或者case語句數小於4則不會採取上面的做法,如果再採用這種方式,那麼會造成較大的資源消耗。這個時候編譯器會採用索引表的方式來進行地址的跳轉。例如: //對應的彙編程式碼 0040B798 mov eax,dword ptr [ebp+8] 0040B79B mov dword ptr [ebp-4],eax 0040B79E mov ecx,dword ptr [ebp-4] ;到此eax = ecx = argc 0040B7A1 sub ecx,1 0040B7A4 mov dword ptr [ebp-4],ecx 0040B7A7 cmp dword ptr [ebp-4],0FEh 0040B7AE ja $L542+0Dh (0040b80b) ;當argc > 255則跳轉到default處 0040B7B0 mov eax,dword ptr [ebp-4] 0040B7B3 xor edx,edx 0040B7B5 mov dl,byte ptr (0040b843)[eax] 0040B7BB jmp dword ptr [edx*4+40B82Bh] 11: case 1: 12: printf("argc = 1n"); 0040B7C2 push offset string "argc = 1n" (00420fb4) 0040B7C7 call printf (00401090) 0040B7CC add esp,4 13: break; 0040B7CF jmp $L542+1Ah (0040b818) 14: case 2: 15: printf("argc = 2n"); 0040B7D1 push offset string "argc = 3n" (00420fa8) 0040B7D6 call printf (00401090) 0040B7DB add esp,4 16: break; 0040B7DE jmp $L542+1Ah (0040b818) 17: case 5: 18: printf("argc = 5n"); 0040B7E0 push offset string "argc = 5n" (00420f9c) 0040B7E5 call printf (00401090) 0040B7EA add esp,4 19: break; 0040B7ED jmp $L542+1Ah (0040b818) 20: case 6: 21: printf("argc = 6n"); 0040B7EF push offset string "argc < 0n" (0042003c) 0040B7F4 call printf (00401090) 0040B7F9 add esp,4 22: break; 0040B7FC jmp $L542+1Ah (0040b818) 23: case 255: 24: printf("argc = 255n"); 0040B7FE push offset string "argc <= 0n" (0042002c) 0040B803 call printf (00401090) 0040B808 add esp,4 25: default: 26: printf("elsen"); 0040B80B push offset string "argc = %dn" (0042001c) 0040B810 call printf (00401090) 0040B815 add esp,4 27: break; 28: } 29: 30: return 0; 0040B818 xor eax,eax 這段程式碼與上述的線性表相比較區別並不大,只是多了一句 「mov dl,byte ptr (0040b843)[eax]」 這似乎又是一個數組,通過查看記憶體可以知道這個數組的值分別為:00 01 05 05 02 03 05 05 … 04,下一句根據這些值在另外一個數組中查找數據,我們列出另外一個數組的值:   C2 B7 40 00   D1 B7 40 00  E0 B7 40 00  EF B7 40 00  FE B7 40 00  0B B8 40 00 通過對比我們發現,這些值分別是每個case與default入口處的地址,編譯器先查找到每個值在數組中對應的元素位置,然後根據這個位置值再在地址表中從、找到地址進行跳轉,這個過程可以用下面的圖來表示:
switch(argc) {      case 1:          printf("argc = 1n");          break;      case 2:          printf("argc = 2n");          break;      case 5:          printf("argc = 5n");          break;      case 6:          printf("argc = 6n");          break;      case 255:          printf("argc = 255n");      default:          printf("elsen");          break;  }

  這樣通過一個每個元素佔一個位元組的表,來表示對應的case在地址表中所對應的位置,從而跳轉到對應的地址,這樣通過對每個case增加一個位元組的記憶體消耗來達到,減少地址表對應的記憶體消耗。

在上述的彙編程式碼中,是利用dl暫存器來存儲對應case在地址表中項,這樣就會產生一個問題,當case 值大於 255,也就是超出了一個位元組的,超出了dl暫存器的表示範圍時,又該如何來進行跳轉這個時候編譯器會採用判定樹的方式來進行判定,在根節點保存的是所有case值的中位數, 左子樹都是大於這個大於這個值的數,右字數是小於這個值的數,通過每次的比較來得到正確的地址。比如下面的這個判定樹,首先與10進行比較,根據與10 的大小關係進入左子樹或者右子樹,再看看左右子樹的分支是否不大於3,若不大於3則直接轉化為對應的if…else if… else結構,大於3則檢測分支是否滿足上述的優化條件,滿足則進行對應的地址表或者索引表的優化,否則會再次對子樹進行優化,以便減少比較次數。

2.3 總結

switch-case會生成一個跳轉表來指示實際的case分支的地址,而這個跳轉表的索引號與switch變數的值是相等的。從而,switch-case不用像if-elseif那樣遍歷條件分支直到命中條件,而只需訪問對應索引號的表項從而到達定位分支的目的。 具體地說,switch-case會生成一份表項數為case量+1的跳錶,程式首先判斷switch變數是否大於(小於)最大(最小)case 常量,若大於(小於),則跳到default分支處理;否則取得索引號為switch變數大小的跳錶項的地址(即跳錶的起始地址+表項大小*索引號),程式接著跳到此地址執行,到此完成了分支的跳轉。

  由此看來,switch-case結構有一點以空間換時間的意思,當分支較多的時候明顯switch-case結構的實行效率會高很多。但是switch-case的缺點是只能處理常量的匹配,在僅有常量選擇分支的時候,可以選用switch-case結構,而此時通過遍曆數組比較更是不可取的一種方式,但是if-elseif可以應用於更多的場合(如a > 10 && a < 20),顯得更為靈活

三、簡單優化

暫且不說if-else與switch相比哪一個的執行效率高,先就知道原理後,我們應如何去優化。

3.1 if-else

對於if-else,在系統是自上而下逐個條件去判斷,直到命中;所以應將機率大的條件置於最前面。以下給出一個簡單例子

double random = Math.random()*100;//生成0-100的隨機數  if(random > 10){ //90%    }else if(random > 5){//5%    }else{//剩下的5%    } 

對於條件機率相等或是條件個數非常多的情況,因為switch的執行時間與條件數量無關,他是根據switch值直接跳轉到對應分支,所以可以選擇switch代替if-else。

3.2 switch

對於switch,實際上是根據case最小值與最大值,維繫了一段連續的記憶體空間,以空間換取時間。以下給出一個簡單的反例,最大值與最小值跨度較大,且之間沒有更多的條件情況,那個無疑實際申請的很多空間是沒用的,所以就應考慮使用if-else在代替。

int random = (int) Math.random() * 100;// 生成0-100的隨機數  switch (random) {  case 0:  break;  case 100:  break;  }