深刻淺出計算機組成原理學習筆記:第二十五講

1、引子

一、取指令(IF)和指令譯碼(ID)的階段,是不須要停頓的

過去三講,我主要爲你介紹告終構冒險和數據冒險,以及增長資源、流水線停頓、操做數前推、亂序執行,這些解決各類「冒險」的技術方案。java

在結構冒險和數據冒險中,你會發現,全部的流水線停頓操做都要從 指令執行階段開始。流水線的前兩個階段,也就是取指令(IF)和指令譯碼(ID)的階段,是不須要停頓的。CPU
會在流水線裏面直接去取下一條指令,而後進行譯碼。算法

二、一旦遇到 if…else 這樣的條件分支,或者 for/while 循環就會不成立

取指令和指令譯碼不會須要遇到任何停頓,這是基於一個假設。這個假設就是,全部的指令代碼都是順序加載執行的。不過這個假設,在執行的代碼中,一旦遇到 if…else 這樣的條件分支,
或者 for/while 循環,就會不成立。oop

咱們先來回顧一下,第6講裏講的cmp比較指令、jmp和jle這樣的條件跳轉指令。能夠看到,在jmp指令發生的時候,CPU可能會跳轉去執行其餘指令。jmp後的那一條指令是否應該順序加載執行,
在流水線裏面進行取指令的時候,咱們無法知道。要等jmp指令執行完成,去更新了PC寄存器以後,咱們才能知道,是否執行下一條指令,仍是跳轉到另一個內存地址,去取別的指令性能

三、如何解決停頓

這種爲了確保能取到正確的指令,而不得不進行等待延遲的狀況,就是今天咱們要講的 控制冒險(ControlHarzard)。這也是流水線設計裏最後一種冒險測試

2、分支預測:今天下雨了,明天還會繼續下雨麼?

在遇到了控制冒險以後,咱們的CPU具體會怎麼應對呢?除了流水線停頓,等待前面的jmp指令執行完成以後,再去取最新的指令,還有什麼好辦法嗎?固然是有的。咱們一塊兒來看一看。spa

一、縮短分支延遲

一、縮短分支延遲

第一個辦法,叫做 縮短分支延遲。回想一下咱們的條件跳轉指令,條件跳轉指令其實進行了兩種電路操做。命令行

第一種,是進行條件比較。這個條件比較,須要的輸入是,根據指令的opcode,就能確認的條件碼寄存器。設計

第二種,是進行實際的跳轉,也就是把要跳轉的地址信息寫入到PC寄存器。不管是opcode,仍是對應的條件碼寄存器,仍是咱們跳轉的地址,都是在指令譯碼(ID)的階段就能得到的。
而對應的條件碼比較的電路,只要是簡單的邏輯門電路就能夠了,並不須要一個完整而複雜的ALU。3d

二、把一些計算結果更早地反饋到流水線中

因此,咱們能夠將條件判斷、地址跳轉,都提早到指令譯碼階段進行,而不須要放在指令執行階段。對應的,咱們也要在CPU裏面設計對應的旁路,在指令譯碼階段,就提供對應的判斷比較的電路。code

這種方式,本質上和前面數據冒險的操做數前推的解決方案相似,就是在硬件電路層面,把一些計算結果更早地反饋到流水線中。這樣反饋變得更快了,後面的指令須要等待的時間就變短了。

三、不過只是改造硬件,並不能完全解決問題

不過只是改造硬件,並不能完全解決問題。跳轉指令的比較結果,仍然要在指令執行的時候才能知道。在流水線裏,第一條指令進行指令譯碼的時鐘週期裏,咱們其實就要去取下一條指令了。
這個時候,咱們其實尚未開始指令執行階段,天然也就不知道比較的結果。

二、分支預測

因此,這個時候,咱們就引入了一個新的解決方案,叫做 分支預測(Branch Prediction)技術,也就是說,讓咱們的CPU來猜一猜,條件跳轉後執行的指令,應該是哪一條。

最簡單的分支預測技術,叫做「 僞裝分支不發生」。顧名思義,天然就是仍然按照順序,把指令往下執行。其實就是CPU預測,條件跳轉必定不發生。這樣的預測方法,
其實也是一種 靜態預測技術。就好像猜硬幣的時候,你一直猜正面,會有50%的正確率。

若是分支預測是正確的,咱們天然賺到了。這個意味着,咱們節省下來原本須要停頓下來等待的時間。若是分支預測失敗了呢?那咱們就把後面已經取出指令已經執行的部分,給丟棄掉。
這個丟棄的操做,在流水線裏面,叫做Zap或者Flush。CPU不只要執行後面的指令,對於這些已經在流水線裏面執行到一半的指令,

咱們還須要作對應的清除操做。好比,清空已經使用的寄存器裏面的數據等等,這些清除操做,也有必定的開銷。

因此,CPU須要提供對應的丟棄指令的功能,經過控制信號清除掉已經在流水線中執行的指令。只要對應的清除開銷不要太大,咱們就是划得來的。

 

 

三、動態分支預測

一、動態分支在天氣預報中的應用

第三個辦法,叫做 動態分支預測。上面的靜態預測策略,看起來比較簡單,預測的準確率也許有50%。可是若是運氣很差,可能就會特別差。

因而,工程師們就開始思考,咱們有沒有更好的辦法呢?好比,根據以前條件跳轉的比較結果來預測,是否是會更準一點?

咱們平常生活裏,最常常會遇到的預測就是天氣預報。若是沒有氣象臺給你天氣預報,你想要猜一猜明天是否是下雨,你會怎麼辦?

有一個簡單的策略,就是徹底根據今天的天氣來猜。若是今天下雨,咱們就預測明天下雨。若是今每天晴,就預測明天也不會下雨。這是一個很符合咱們平常生活經驗的預測。由於通常下雨天,
都是連着下幾天,不斷地間隔地發生「天晴-下雨-天晴-下雨」的狀況並很少見。

那麼,把這樣的實踐拿到生活中來是否是有效呢?我在這裏給了一張2019年1月上海的天氣狀況的表格。

咱們用前一天的是否是下雨,直接來預測後一天會不會下雨。這個表格裏一共有31天,那咱們就能夠預測30次。你能夠數一數,按照這種預測方式,咱們能夠預測正確23次,正確率是76.7%,
比隨機預測的50%要好上很多。

二、一樣的策略,能夠放在分支預測上

而一樣的策略,咱們同樣能夠放在分支預測上。這種策略,咱們叫 一級分支預測(One Level BranchPrediction),或者叫 1比特飽和計數(1-bit saturating counter)。這個方法,其實就是用一個比特,去記
錄當前分支的比較狀況,直接用當前分支的比較狀況,來預測下一次分支時候的比較狀況。

三、狀態機

只用一天下雨,就預測次日下雨,這個方法仍是有些「草率」,咱們能夠用更多的信息,而不僅是一次的分支信息來進行預測。因而,咱們能夠引入一個 狀態機(State Machine)來作這個事情。
若是連續發生下雨的狀況,咱們就認爲更有可能下雨。以後若是隻有一天放晴了,咱們仍然認爲會下雨。在連續下雨以後,要連續兩天放晴,咱們纔會認爲以後會放晴。整個狀態機的流轉,能夠參考我在文稿裏放的圖。

 

 

這個狀態機裏,咱們一共有4個狀態,因此咱們須要2個比特來記錄對應的狀態。這樣這整個策略,就能夠叫做 2比特飽和計數,或者叫 雙模態預測器(Bimodal Predictor)。

好了,如今你能夠用這個策略,再去對照一下上面的天氣狀況。若是天氣的初始狀態咱們放在「多半放晴」的狀態下,咱們預測的結果的正確率會是22次,也就是73.3%的正確率。能夠看到,並非更復雜的算
法,效果必定就更好。實際的預測效果,和實際執行的指令高度相關。

若是想對各類分支預測技術有所瞭解,Wikipedia裏面有更詳細的內容和更多的分支預測算法,你能夠看看。

3、爲何循環嵌套的改變會影響性能?

一樣循環了十億次,第一段程序只花了5毫秒,而第二段程序則花了15毫秒,足足多了2倍。

說完了分支預測,如今咱們先來看一個Java程序

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");
    }
}

這是一個簡單的三重循環,裏面沒有任何邏輯代碼。咱們用兩種不一樣的循環順序各跑一次。第一次,最外重循環循環了100次,第二重循環1000次,最內層的循環了10000次。第二次,
咱們把順序倒過來,最外重循環10000次,第二重仍是1000次,最內層100次。

事實上,這段代碼在這個專欄一開始的幾講裏面,就有同窗來提問,想要弄明白這裏面的關竅。你能夠先猜一猜,這樣兩次運行,花費的時間是同樣的麼?結果應該會讓你大吃一驚。咱們能夠看看對應的命令行輸出。

Time spent in first loop is 5ms
Time spent in second loop is 15ms

一樣循環了十億次,第一段程序只花了5毫秒,而第二段程序則花了15毫秒,足足多了2倍。

這個差別就來自咱們上面說的分支預測。咱們在前面講過,循環其實也是利用cmp和jle這樣先比較後跳轉的指令來實現的。若是對for循環的彙編代碼或者機器代碼的實現不太清楚,
你能夠回頭去複習一下第6講。這裏的代碼,每一次循環都有一個cmp和jle指令。每個 jle 就意味着,要比較條件碼寄存器的狀態,決定是順序執行代碼,

仍是要跳轉到另一個地址。也就是說,在每一次循環發生的時候,都會有一次「分支」。

分支預測策略最簡單的一個方式,天然是「 假定分支不發生」。對應到上面的循環代碼,就是循環始終會進行下去。在這樣的狀況下,上面的第一段循環,也就是內層 k 循環10000次的代碼。每隔10000次,
纔會發生一次預測上的錯誤。而這樣的錯誤,在第二層 j 的循環發生的次數,是1000次。最外層的 i 的循環是100次。每一個外層循環一次裏面,都會發生1000次最內層 k 的循環的預測錯誤,因此一共會發生 100 × 1000 = 10萬次預測錯誤。

上面的第二段循環,也就是內存k的循環100次的代碼,則是每100次循環,就會發生一次預測錯誤。這樣的錯誤,在第二層j的循環發生的次數,仍是1000次。最外層 i 的循環是10000次,因此一共會發生 1000 ×10000 = 1000萬次預測錯誤。

到這裏,相信你能猜到爲何一樣空轉次數相同的循環代碼,第一段代碼運行的時間要少得多了。由於第一段代碼發生「分支預測」錯誤的狀況比較少,更多的計算機指令,
在流水線裏順序運行下去了,而不須要把運行到一半的指令丟棄掉,再去從新加載新的指令執行。

4、總結延伸

好了,這一講,我給你講解了什麼是控制冒險,以及應對控制冒險的三個方式。

第一種方案,相似咱們的操做數前推,實際上是在改造咱們的CPU功能,經過增長對應的電路的方式,來縮短分支帶來的延遲。另外兩種解決方案,不管是「僞裝分支不發生」,仍是「動態分支預測」,
其實都是在進行「分支預測」。只是,「僞裝分支不發生」是一種簡單的靜態預測方案而已。

在動態分支預測技術裏,我給你介紹了一級分支預測,或者叫1比特飽和計數的方法。其實就是認爲,預測結果和上一次的條件跳轉是一致的。在此基礎上,我還介紹了利用更多信息的,就是2比特飽和計數,
或者叫雙模態預測器的方法。這個方法其實也只是經過一個狀態機,多看了一步過去的跳轉比較結果。

這個方法雖然簡單,可是卻很是有效。在 SPEC 89 版本的測試當中,使用這樣的飽和計數方法,預測的準確率可以高達93.5%。Intel的CPU,一直到Pentium時代,在尚未使用MMX指令集的時候,
用的就是這種分支預測方式。

這一講的最後,我給你看了一個有意思的例子。經過交換內外循環的順序,咱們體驗了一把控制冒險致使的性能差別。雖然執行的指令數是同樣的,可是分支預測失敗得多的程序,性能就要差上幾倍。

相關文章
相關標籤/搜索