順序、條件、迴圈語句的底層解釋
順序結構
資料傳送指令
我們都清楚,絕大多數編譯器都把組合語言作為中間語言,把組合語言程式變成可執行的二進位制檔案早就解決了,所以現在的高階語言基本上只需要把自己翻譯成組合語言就可以了。
彙編指令總共只有那麼多,大多數指令都是對資料進行操作,比如常見的資料傳送指令mov
。不難理解,被操作資料無非有三種形式,立即數,即用來表示常數值;暫存器,此時的資料即存放在指定暫存器中的內容;記憶體引用,它會根據計算出來的地址訪問某個記憶體位置。
需要注意的是,到了彙編層級,就不像高階語言那樣隨隨便便int
就能和long
型別的資料相加減,他們在底層所佔有的位元組是不一樣的,彙編指令是區分操作資料大小的,比如資料傳送指令,就有下面這些品種(x86-64 對資料傳送指令加了一條限制:兩個運算元不能都指向記憶體位置)。
壓棧與彈棧
對於棧,我想不必多講,IT 行業的同學都清楚,它是一種線性資料結構,其中的資料遵循“先進後出”原則,暫存器%rsp
儲存著棧頂元素的地址,即棧頂指標。一個程式要執行起來,離不開棧這種資料結構。
棧使用最多的就是彈棧popq
和壓棧pushq
操作。比如將一個四字值壓入棧中,棧頂指標首先要減 8(棧向下增長),然後將值寫到新的棧頂地址;而彈棧則需要先將棧頂資料讀出,然後再將棧指標加 8。所以pushq
和popq
指令就可以表示為下面的形式。
// 壓棧
subq $8, %rsp
movq %rbp, (%rsp)
// 彈棧
movq (%rsp), %rax
addq $8 , %rsp
複製程式碼
其他還有算術、邏輯、載入有效地址、移位等等指令,可以查閱相關文件瞭解,不作過多介紹,彙編看起來確實枯燥乏味。
條件結構
前面講的都是順序結構,我們的程式中不可能只有順序結構,條件結構是必不可缺的元素,那麼彙編又是如何實現條件結構的呢?
首先你需要知道,除了整數暫存器,CPU 還維護著一組條件碼暫存器,我們主要是瞭解如何把高階語言的條件結構轉換為組合語言,不去關注這些條件碼暫存器,只需要知道彙編可以通過檢測這些暫存器來執行條件分支指令。
if-else 語句
下面是 C 語言中的if-else
語句的通用形式。
if(test-expr){
then-statement
}else {
else-statement
}
複製程式碼
組合語言通常會將上面的 C 語言模板轉換為下面的控制流形式,只要使用條件跳轉和無條件跳轉,這種形式的控制流就可以和彙編程式碼一一對應,我們以 C 語言形式給出。
t = test-expr;
if(!t){
goto false;
}
then-statement;
goto done;
false:
else-statement;
done:
複製程式碼
但是這種條件控制轉移形式的程式碼在現代處理器上可能會很低效。原因是它無法事先確定要跳轉到哪個分支,我們的處理器通過流水線來獲得高效能,流水線的要求就是事先明確要執行的指令順序,而這種形式的程式碼只有當條件分支求值完成後,才能決定走哪一個分支。即使處理器採用了非常精密的分支預測邏輯,但是還是有錯誤預測的情況,一旦預測錯誤,那將會浪費 15 ~ 30 個時鐘週期,導致效能下降。
在流水線中,把一條指令分為多個階段,每個階段只執行所需操作的一小部分,比如取指令、確定指令型別、讀資料、運算、寫資料以及更新程式計數器。流水線通過重疊連續指令的步驟來獲得高效能,比如在取一條指令的同時,執行它前面指令的算術運算。所以如果事先不知道指令執行順序,那麼事先所做的預備工作就白乾了。
為了提高效能,可以改寫成使用條件資料傳送的程式碼,比如下面的例子。
v = test-expr ? then-expr : else-expr;
// 使用條件資料傳送方法
v = then-expr;
ve = else-expr;
t = test-expr;
if(!t){
v = ve;
}
複製程式碼
這樣改寫,就能提高程式的效能了,但是並不是所有的條件表示式都可以使用條件傳送來編譯,一般只有當兩個表示式都很容易計算時,編譯器才會採用條件資料傳送的方式,大部分都還是使用條件控制轉移方式編譯。
switch 語句
switch
語句可以根據一個整數索引值進行多重分支,在處理具有多種可能結果的測試時,這種語句特別有用。為了讓switch
的實現更加高效,使用了一種叫做跳轉表的資料結構(Radis 也是用的跳錶)。跳轉表是一個數組,表項 i 是一個程式碼段的地址,當開關情況數量比較多的時候,就會使用跳轉表。
我們舉個例子,還是採用 C 語言的形式表是控制流,要理解的是執行switch
語句的關鍵步驟就是通過跳轉表來訪問程式碼的位置。
void switch_eg(long x, long n, long *dest){
long val = x;
switch(n){
case 100:
val *= 13;
break;
case 102:
val += 10;
case 103:
val += 11;
break;
case 104:
case 105:
val *= val;
break;
default:
val = 0;
}
*dest = val;
}
複製程式碼
要注意的是,上面的程式碼中有的分支沒有break
,這種問題在筆試中會經常遇到,沒有break
會繼續執行下面的語句,即變成了順序執行。上面的程式碼會被翻譯為下面這種控制流。
void switch_eg(long x, long n, long *dest){
static void *jt[7] = {
&&loc_A, &&loc_def, &&loc_B,
&&loc_C, &&loc_D, &&loc_def,
&&loc_D
};
unsigned long index = n - 100;
long val;
if(index > 6){
goto loc_def;
}
goto *jt[index];
loc_A:
val = x * 13;
goto done;
loc_B:
x = x + 10;
loc_C:
val = x + 11;
goto done;
loc_D:
val = x * x;
goto done;
loc_def:
val = 0;
done:
*dest = val;
}
複製程式碼
迴圈結構
C 語言中有do-while
、while
和for
三種迴圈結構,它們的通用形式一般都長下面那樣。
// do-while
do
body-statement
while(test-expr);
// while
while(test-expr)
body-statement
// for
for(init-expr; test-expr; update-expr)
body-statement
複製程式碼
do-while
的特點是body-statement
一定會執行一次,所以我們可以將do-while
翻譯成下面的控制流形式,很容易就能聯想到它的彙編形式。
loop:
body-statement;
t = test-expr;
if(t){
goto loop;
}
複製程式碼
while
迴圈我們給出兩種形式的控制流,其中一種包含do-while
形式,如下所示。
// 第一種形式
t = test-expr;
if(!t){
goto done;
}
do
body-statement;
while(test-expr);
done:
// 第二種形式
goto test;
loop:
body-statement;
test:
t = test-expr;
if(t){
goto loop;
}
複製程式碼
面試的時候,有的面試官會問你for
迴圈的執行順序,現在深入理解了三種迴圈的機制,再也不怕面試官啦。for
迴圈可以轉換成如下的while
形式。
init-expr;
while(test-expr){
body-statement;
update-expr;
}
複製程式碼
有了這種形式的for
迴圈,我們只需要將其中的while
部分再翻譯一下就好了,前文給出了兩種while
翻譯的方式,而具體採用哪種方式,取決於編譯器優化的等級。
總結
計算機就是用那麼幾條簡簡單單的指令就完成了各種複雜的操作,不得不折服於電腦科學家們的魅力。現在人工智慧被炒的很火熱,然後人是事件、情感驅動的,而計算機是控制流驅動的,所以從架構上就決定了,馮諾依曼體系計算機實現的都是弱人工智慧。