淺談if...else與switch的區別和效率問題

原文地址:https://www.jeremyjone.com/546/ ,轉載請註明!


各種語言都有條件語句,基本上所有語言都有if...else,大部分語言也都有switch,他們長得很像,基本上都可以相互轉化使用。那麼這兩種語句有什麼區別呢?

if…else

首先說說if...else,與其變種if...else if...else,部分語言可能長得不太一樣,但是大同小異。

我們通常是這樣使用的:

int a = 2;

if (a == 1) {
	cout << "a = 1" << endl;
}
else if (a == 2) {
	cout << "a = 2" << endl;
}
else {
	cout << "other number" << endl;
}

這個很好理解,它會從上至下依次進行判斷,當有一個條件滿足時,進入其中執行代碼,執行完成後跳出整個條件語句。

這樣的話,當條件很多時,比如10個,100個,它需要從上至下一條一條進行判斷,當恰好最後一個條件命中滿足時,這是多麼恐怖的事情。

所以,使用if...else語句時需要注意,我們需要把命中率高的條件放在最前面。

switch

相比之下,有時候可能我們更喜歡switch語句的格式整齊,當然,也有很多公司對使用switch有很多要求甚至禁用,這都是語法規範層面,我們來看看效率。

通常,我們是這樣使用switch的:

int a = 2;

switch (a)
{
case 1:
	cout << "a = 1" << endl;
	break;
case 2:
	cout << "a = 2" << endl;
	break;
default:
	cout << "other number" << endl;
	break;
}

看上去確實和if...else差不多,他們確實也可以互相轉化。那麼如何計算他們的執行邏輯呢?我們看一下switch的彙編:

	switch (a)
00AC5E8F 8B 45 F8             mov         eax,dword ptr [a]  
00AC5E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00AC5E98 83 BD 30 FF FF FF 01 cmp         dword ptr [ebp-0D0h],1  
00AC5E9F 74 0B                je          main+4Ch (0AC5EACh)  
00AC5EA1 83 BD 30 FF FF FF 02 cmp         dword ptr [ebp-0D0h],2  
00AC5EA8 74 2D                je          main+77h (0AC5ED7h)  
00AC5EAA EB 56                jmp         main+0A2h (0AC5F02h)

很明顯,這段和if...else差不多,同樣是比較(cmp)了兩次,如果相等跳轉(je)到指定內存,如果不等,執行最後一條無條件跳轉(jmp)跳出當前條件語句。

我們發現這個和if...else還真是差不多。那麼多一些條件:

switch條件數較多情況

int a = 2;

switch (a)
{
case 1: 
	cout << "a = 1" << endl;
	break;
case 2:
	cout << "a = 2" << endl;
	break;
case 3:
	cout << "a = 3" << endl;
	break;
case 4: 
	cout << "a = 4" << endl;
	break;
case 5: 
	cout << "a = 5" << endl;
	break;
case 6:
	cout << "a = 6" << endl;
	break;
default:
	cout << "other number" << endl;
	break;
}

再來看一下這段代碼的彙編:

	switch (a)
00C45E8F 8B 45 F8             mov         eax,dword ptr [a]  
00C45E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00C45E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00C45E9E 83 E9 01             sub         ecx,1  
00C45EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00C45EA7 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5  
00C45EAE 0F 87 18 01 00 00    ja          $LN9+2Bh (0C45FCCh)  
00C45EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00C45EBA FF 24 95 0C 60 C4 00 jmp         dword ptr [edx*4+0C4600Ch]  

啊哈,我們發現變化很大,只有一個比較(cmp)了,也只有一個小於跳轉(ja),同時多了一個減法(sub)操作。

這是因爲我們的編譯器很聰明,它能在編譯時發現不同情況(下面還有更多情況)。當編譯器看到switch語句時,首先進行一大段操作,之後就是不同條件下的操作,可以不用關心。

在這種條件較多的情況下,編譯器首先會將所有條件排序,減去最小條件值(這裏是1),然後比較條件最大值與條件最小值的差值(這裏是5),如果比這個數大,則直接跳轉到default語句,否則執行最後一句的jmp dword ptr [edx*4+0C4600Ch],我們發現每次它都會乘4,這是因爲int在x86架構中佔4字節。也就是說,從內存的0C4600Ch位置開始,存放了條件1的操作地址,依次往後爲條件2、條件3…的操作地址。

這樣的話,就能完美執行1-6的各種條件。

這樣有幾種比較特殊的情況:

起始條件如果不爲1

根據上面思路,減去相應的起始數字即可。比如起始爲2,則會每次減去2。

	switch (a)
00615E8F 8B 45 F8             mov         eax,dword ptr [a]  
00615E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00615E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00615E9E 83 E9 02             sub         ecx,2  
00615EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00615EA7 83 BD 30 FF FF FF 04 cmp         dword ptr [ebp-0D0h],4  
00615EAE 0F 87 EA 00 00 00    ja          $LN8+2Bh (0615F9Eh)  
00615EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00615EBA FF 24 95 E0 5F 61 00 jmp         dword ptr [edx*4+615FE0h]

很明顯我們看到減法操作(sub)中每次都會減去2,這是因爲我們起始值爲2的緣故,當然,比較的數字也從5變成了4,因爲我們最大的條件爲6,6-2爲4。

中間不連續

比如我們判斷a = 3,並查找a,在條件2和條件4之間沒有條件3,彙編爲:

	switch (a)
00C75E8F 8B 45 F8             mov         eax,dword ptr [a]  
00C75E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00C75E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00C75E9E 83 E9 01             sub         ecx,1  
00C75EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00C75EA7 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5  
00C75EAE 0F 87 EA 00 00 00    ja          $LN8+2Bh (0C75F9Eh)  
00C75EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00C75EBA FF 24 95 E0 5F C7 00 jmp         dword ptr [edx*4+0C75FE0h] 

這時編譯器也很聰明,爲了保持上面的操作,它會在條件3的地址中保存default語句的操作地址,編譯器也可以通過最後一句jmp dword ptr [edx*4+0C75FE0h]直接找到改地址,並繼續執行default對應的操作。

對應的條件3的地址取出來爲:0x00C75FE8 9e 5f c7 00,對應的地址就是:00c75f9e,操作的彙編爲:

	default:
		cout << "other number" << endl;
00C75F9E 8B F4                mov         esi,esp  
00C75FA0 68 A3 12 C7 00       push        offset std::endl<char,std::char_traits<char> > (0C712A3h)  
00C75FA5 68 04 9C C7 00       push        offset string "a = 10" (0C79C04h)  
00C75FAA A1 D4 D0 C7 00       mov         eax,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0C7D0D4h)]  
00C75FAF 50                   push        eax  
00C75FB0 E8 53 B2 FF FF       call        std::operator<<<std::char_traits<char> > (0C71208h)  
00C75FB5 83 C4 08             add         esp,8  
00C75FB8 8B C8                mov         ecx,eax  
00C75FBA FF 15 A0 D0 C7 00    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0C7D0A0h)]  
00C75FC0 3B F4                cmp         esi,esp  
00C75FC2 E8 B9 B2 FF FF       call        __RTC_CheckEsp (0C71280h) 

我們看到從計算出來的地址可以直接跳轉到default操作。

上述是特殊情況中比較普通的,還有幾種更爲特殊的,編譯器也會更加聰明:

條件的差值較大

比如,我們有這樣的代碼:

int a = 2;

switch (a)
{
case 1: 
	cout << "a = 1" << endl;
	break;
case 2:
	cout << "a = 2" << endl;
	break;
case 3:
	cout << "a = 3" << endl;
	break;
case 4: 
	cout << "a = 4" << endl;
	break;
case 5: 
	cout << "a = 5" << endl;
	break;
case 6:
	cout << "a = 6" << endl;
	break;
case 100:
	cout << "a = 100" << endl;
	break;
default:
	cout << "other number" << endl;
	break;
}

我們再來看一下彙編:

	switch (a)
00335E8F 8B 45 F8             mov         eax,dword ptr [a]  
00335E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
00335E98 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
00335E9E 83 E9 01             sub         ecx,1  
00335EA1 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
00335EA7 83 BD 30 FF FF FF 63 cmp         dword ptr [ebp-0D0h],63h  
00335EAE 0F 87 4D 01 00 00    ja          $LN10+2Bh (0336001h)  
00335EB4 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
00335EBA 0F B6 82 60 60 33 00 movzx       eax,byte ptr [edx+336060h]  
00335EC1 FF 24 85 40 60 33 00 jmp         dword ptr [eax*4+336040h] 

發現好像跟之前差不多,依舊是計算地址,但是不同的是在跳轉之前多了一條movzx eax,byte ptr [edx+336060h],那我們就查看一下336060長什麼樣子:

0x00336060  00 01 02 03  ....
0x00336064  04 05 07 07  ....
0x00336068  07 07 07 07  ....
0x0033606C  07 07 07 07  ....
0x00336070  07 07 07 07  ....
0x00336074  07 07 07 07  ....
0x00336078  07 07 07 07  ....
0x0033607C  07 07 07 07  ....
0x00336080  07 07 07 07  ....
0x00336084  07 07 07 07  ....
0x00336088  07 07 07 07  ....
0x0033608C  07 07 07 07  ....
0x00336090  07 07 07 07  ....
0x00336094  07 07 07 07  ....
0x00336098  07 07 07 07  ....
0x0033609C  07 07 07 07  ....
0x003360A0  07 07 07 07  ....
0x003360A4  07 07 07 07  ....
0x003360A8  07 07 07 07  ....
0x003360AC  07 07 07 07  ....
0x003360B0  07 07 07 07  ....
0x003360B4  07 07 07 07  ....
0x003360B8  07 07 07 07  ....
0x003360BC  07 07 07 07  ....
0x003360C0  07 07 07 06  ....

有沒有很眼熟,這不就是單字節的數組麼?那麼這些數字也可以很好理解,這就是對應的不同操作,猜也能猜到,07表示default操作,之前從00-06對應相應的操作,取出這些數字後在進行dword ptr [eax*4+336040h]操作,則得到操作地址。

數字差值很大很大

剛纔差值是100左右,編譯器感覺不夠大,我們來個更大的:

int a = 2;

switch (a)
{
case 1: 
	cout << "a = 1" << endl;
	break;
case 2:
	cout << "a = 2" << endl;
	break;
case 3:
	cout << "a = 3" << endl;
	break;
case 4: 
	cout << "a = 4" << endl;
	break;
case 5: 
	cout << "a = 5" << endl;
	break;
case 6:
	cout << "a = 6" << endl;
	break;
case 10000:
	cout << "a = 10000" << endl;
	break;
default:
	cout << "other number" << endl;
	break;
}

這次比較10000,再來看一下彙編的變化:

	switch (a)
006D5E8F 8B 45 F8             mov         eax,dword ptr [a]  
006D5E92 89 85 30 FF FF FF    mov         dword ptr [ebp-0D0h],eax  
006D5E98 81 BD 30 FF FF FF 10 27 00 00 cmp         dword ptr [ebp-0D0h],2710h  
006D5EA2 7F 39                jg          main+7Dh (06D5EDDh)  
006D5EA4 81 BD 30 FF FF FF 10 27 00 00 cmp         dword ptr [ebp-0D0h],2710h  
006D5EAE 0F 84 3C 01 00 00    je          $LN9+2Bh (06D5FF0h)  
006D5EB4 8B 8D 30 FF FF FF    mov         ecx,dword ptr [ebp-0D0h]  
006D5EBA 83 E9 01             sub         ecx,1  
006D5EBD 89 8D 30 FF FF FF    mov         dword ptr [ebp-0D0h],ecx  
006D5EC3 83 BD 30 FF FF FF 05 cmp         dword ptr [ebp-0D0h],5  
006D5ECA 0F 87 4B 01 00 00    ja          $LN9+56h (06D601Bh)  
006D5ED0 8B 95 30 FF FF FF    mov         edx,dword ptr [ebp-0D0h]  
006D5ED6 FF 24 95 5C 60 6D 00 jmp         dword ptr [edx*4+6D605Ch]  
006D5EDD E9 39 01 00 00       jmp         $LN9+56h (06D601Bh)  

這次變化就很大了,編譯器不會允許創建一個10000字節的連續內存,它會首先跟最大值進行比較,然後有條件跳轉到較小的分段空間中,最後根據條件找到對應的操作地址。

總結

相比if...elseswitch其根本原理還是通過數字直接跳轉,不會依次比較,這在條件規模比較大且比較連續的情況下,switch效率會明顯高於if...else,這也是爲什麼switch只允許傳入整數和字符的原因。

發佈了96 篇原創文章 · 獲贊 30 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章