(三)cpu內部如何處理代碼的執行

1、一個最簡單cpu的數據通路

可以看到,cpu內部一直重複執行着 Fetch(取指令)-->  decode(指令譯碼)--> execute(執行指令),這個循環叫做指令週期pc寄存器中存儲的地址,需要地址譯碼器來尋址,在偌大的內存中找到對應地址存儲的指令後,存入指令寄存器,再通過指令譯碼器把指令翻譯成各個線路的控制信號給到運算器(運算器ALU是沒有狀態的,只能根據輸入計算並輸出結果),運算器輸出結果,再結果寫入到存儲器中。

2、cpu是如何做到自動重複執行,如何實現存儲,如何讓pc寄存器自動增加

我們先來看一下左邊這個簡單的電路,開關B原本是閉合的,我們把開關A合上後,開關B則會不停的斷開和閉合。對於下游電路來說就是不斷產生0和1這樣的信號,這就是我們的時鐘信號。我們只需要在每次電路接通時對pc寄存器加1,即可以實現每間隔一個時鐘週期 pc寄存器就會加1。

有了震盪電路,我們還需要解決數據的存儲問題。請看圖1,開關全部斷開輸出爲0,合上開關R輸出爲1,斷開開關R輸出仍爲1,再合上開關S輸出則變爲0可以看到這個電路開關都斷開時的輸出結果與上一次的操作有關。這就是記憶功能,該電路能存儲一個bit的信息。(如圖2,對電路再做一些完善,加上時鐘信號,並只提供一個輸入端,實現對一個bit的讀寫,我們叫做D型控制器)

有了時鐘信號,D型控制器,再加上加法器即可以實現pc寄存器的自增了。

3、cpu分級設計提升性能

有了時鐘信號,cpu就能實現自動重複執行: Fetch(取指令)-->  decode(指令譯碼)--> execute(執行指令)。每一個時鐘週期,程序計數器就會加1,對於單指令週期處理的方式, 顯然在這個時鐘週期內我們必須完成處理一個指令的所有步驟。因爲下一個時鐘週期來臨,我們必須要處理下一條指令了。這種處理指令的方式很簡單,但是有一個最大的弊端,一個時鐘週期必須保證處理完一條最複雜的指令,那麼當處理簡單的指令時就會浪費很多的時間

我們知道在處理指令時,不同的操作階段使用到cpu中不同的組件,我們把這一整套處理操作比作成工廠裏的流水線。我們只需要保證一個時鐘週期內執行完一個最複雜的操作即可,這就是流水線分級處理,如下圖所示。這樣我們就充分的利用了每個組件,提升處理指令的效率。

4、cpu分級後的挑戰

雖然分級後明顯提升了cpu的效率,但是缺也帶來了很多的挑戰。

  • 功耗:分級了後則線路變得複雜,數據的存儲變得更多,功耗資源消耗更大
  • 結構冒險:可能存在多個步驟之間同時執行時爭搶資源
  • 數據冒險:多個指令之間的數據存在依賴關係,先讀後寫,先寫後讀,先寫再寫
  • 出現if else時:指令並非是順序執行的,那麼分級後默認的順序執行就可能出錯

針對這些問題,我們提出了一些解決方案

  • 針對功耗的問題,我們只需要控制好分級的層數即可,目前流水線級數已經達到了14級
  • 針對資源衝突,我們可以增加資源
  • 針對數據冒險,最簡單的辦法就是在指令中插入等待操作NOP。但是單純的停頓等待太過於浪費時間,我們可以使用操作數前推的方式,把上一個計算的結果,直接轉發到下一個指令的執行階段。這樣我們需要多拉出一根信號傳輸線路。但是這樣可以省去把結果寫入寄存器,再讀取出來的步驟的時間。提前執行下一條指令。但是也沒法完全避免等待,畢竟還是需要等待上一個指令把結果計算出來。

  • 亂序執行在等待階段,某個部件其實是空閒的,那麼後面的指令若沒有依賴關係,則可以不用等前面的指令,自己先執行。亂序執行實現比較複雜,大致情況如下:

1、取指令和指令譯碼還是順序執行的,但是譯碼完成後CPU 不會直接進行指令執行,而是進行一次指令分發,把指令發到一個叫作保留站的地方。

2、保留站中的這些指令不會立刻執行,而要等待它們所依賴的數據,傳遞給它們之後纔會執行。一旦指令依賴的數據來齊了,指令就可以交到後面的功能單元,其實就是 ALU,去執行了。我們有很多功能單元可以並行運行,但是不同的功能單元能夠支持執行的指令並不相同。

3、指令執行的階段完成之後,我們並不能立刻把結果寫回到寄存器裏面去,而是把結果再存放到一個叫作重排序緩衝區的地方。在重排序緩衝區裏,我們的 CPU 會按照取指令的順序,對指令的計算結果重新排序。只有排在前面的指令都已經完成了,纔會提交指令,完成整個指令的運算結果。

4、 實際的指令的計算結果數據,並不是直接寫到內存或者高速緩存裏,而是先寫入存儲緩衝區(Store Buffer 面,最終纔會寫入到高速緩存和內存裏。

  • 如果存在 if else 這種代碼,那麼取指令和譯碼就不能沒有停頓一直執行下去了。因爲我們無法得知下一條指令存儲的地址。if else 的邏輯對應到指令 cmp(比較), jmp(跳轉)。只有等到 jmp 執行完後去更新了pc寄存器,我們才能去取下一條指令。此處則用到了分支預測方案進行處理:

1、靜態預測,假裝分支不發生,直接往下執行,成功的概率百分之五十,命中率太低。

2、動態預測,根據前面的結果來判斷後面的分支跳轉,成功的概率大大提高。 例如for 循環,第一次不跳轉,則預測下一次也不跳轉。

3、預測錯誤處理當上一條指令真正的分支判斷結果出來後,發現預測錯誤,則需要清除掉已經執行了一半的操作,重新取指令並譯碼執行。只要去除指令代價不大就是很划算的。

4、分支預測的例子,代碼如下,同樣循環了十億次,第二段程序比第一段程序多花費的好幾倍時間。這個差異就來自我們上面說的分支預測。看下圖可以發現第一段程序預測錯誤10萬次,而第二段程序預測錯誤了1000萬次。

public class BranchPrediction {
    public static void main(String args[]) {        
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            for (int j = 0; j <1000; j ++) {
                for (int k = 0; k < 10000; k++) {
                }
            }
        }
        long end = System.currentTimeMillis();
        System.out.println("Time spent is " + (end - start));
                
        start = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            for (int j = 0; j <1000; j ++) {
                for (int k = 0; k < 100; k++) {
                }
            }
        }
        end = System.currentTimeMillis();
        System.out.println("Time spent is " + (end - start) + "ms");
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章