1. 程式人生 > >順序、條件、迴圈語句的底層解釋

順序、條件、迴圈語句的底層解釋

順序結構

資料傳送指令

    我們都清楚,絕大多數編譯器都把組合語言作為中間語言,把組合語言程式變成可執行的二進位制檔案早就解決了,所以現在的高階語言基本上只需要把自己翻譯成組合語言就可以了。

    彙編指令總共只有那麼多,大多數指令都是對資料進行操作,比如常見的資料傳送指令mov。不難理解,被操作資料無非有三種形式,立即數,即用來表示常數值;暫存器,此時的資料即存放在指定暫存器中的內容;記憶體引用,它會根據計算出來的地址訪問某個記憶體位置。

    需要注意的是,到了彙編層級,就不像高階語言那樣隨隨便便int就能和long型別的資料相加減,他們在底層所佔有的位元組是不一樣的,彙編指令是區分操作資料大小的,比如資料傳送指令,就有下面這些品種(x86-64 對資料傳送指令加了一條限制:兩個運算元不能都指向記憶體位置)。

image

壓棧與彈棧

    對於棧,我想不必多講,IT 行業的同學都清楚,它是一種線性資料結構,其中的資料遵循“先進後出”原則,暫存器%rsp儲存著棧頂元素的地址,即棧頂指標。一個程式要執行起來,離不開棧這種資料結構。

    棧使用最多的就是彈棧popq和壓棧pushq操作。比如將一個四字值壓入棧中,棧頂指標首先要減 8(棧向下增長),然後將值寫到新的棧頂地址;而彈棧則需要先將棧頂資料讀出,然後再將棧指標加 8。所以pushqpopq指令就可以表示為下面的形式。

// 壓棧
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-whilewhilefor三種迴圈結構,它們的通用形式一般都長下面那樣。

// 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翻譯的方式,而具體採用哪種方式,取決於編譯器優化的等級。

總結

    計算機就是用那麼幾條簡簡單單的指令就完成了各種複雜的操作,不得不折服於電腦科學家們的魅力。現在人工智慧被炒的很火熱,然後人是事件、情感驅動的,而計算機是控制流驅動的,所以從架構上就決定了,馮諾依曼體系計算機實現的都是弱人工智慧。