計算機組成原理5-預測和冒險


 

 

1、CPU 的流水線設計的三大冒險

    結構冒險(Structural Hazard)、數據冒險(Data Hazard)以及控制冒險(Control Hazard)。

 

2、結構冒險

  結構冒險的本質是硬件層面資源的競爭。CPU 在同一個時鍾周期,同時在運行兩條計算機指令的不同階段。但是這兩個不同的階段,可能會用到同樣的硬件電路。

 

    可以看到,在第 1 條指令執行到訪存(MEM)階段的時候,流水線里的第 4 條指令,在執行取指令(Fetch)的操作。訪存和取指令,都要進行內存數據的讀取。內存只有一個地址譯碼器的作為地址輸入,那就只能在一個時鍾周期里面讀取一條數據,沒辦法同時執行第 1 條指令的讀取內存數據和第 4 條指令的讀取指令代碼。解決這種資源沖突的本質方法就是增加資源,在 CPU 的結構冒險里面。對於訪問內存數據和取指令的沖突,一個直觀的解決方案就是內存分成兩部分,讓它們各有各的地址譯碼器。這兩部分分別是存放指令的程序內存和存放數據的數據內存。

    現代的 CPU 並不會直接讀取主內存。它會從主內存把指令和數據加載到高速緩存中,這樣后續的訪問都是訪問高速緩存。而指令緩存和數據緩存的拆分,使得 CPU 在進行數據訪問和取指令的時候,不會再發生資源沖突的問題了。

 

3、數據冒險

    數據冒險是程序邏輯層面的事,有三種類型:分別是先寫后讀(Read After Write,RAW)、先讀后寫(Write After Read,WAR)和寫后再寫(Write After Write,WAW)。

 

    先寫后讀:先把數據寫在內存里面,然后再把這個內存地址里面的數據寫到寄存器里面,先寫后讀所面臨一個數據依賴。如果順序不鞥不能保證,程序就會報錯。這個先寫后讀的依賴關系,一般被稱之為數據依賴

    先讀后寫:把寄存器里面的數據讀出來,再把數據寫進寄存器。先讀后寫的依賴,一般被叫作反依賴

    寫后再寫:寫入內存地址的兩個數據需要有先后順序,這種依賴稱為輸出依賴

 

    同一個寄存器或者內存地址的操作,都有明確強制的順序要求。而這個順序操作的要求,也為使用流水線帶來了很大的挑戰。因為流水線架構的核心,就是在前一個指令還沒有結束的時候,后面的指令就要開始執行。解決這個問題最簡單的辦法就是流水線停頓,也叫做流水線冒泡。

    就是執行后面的指令,進行指令譯碼的時候,會拿到對應指令所需要訪問的寄存器和內存地址。能夠判斷出來,這個指令是否會觸發數據冒險。如果會觸發數據冒險,插入無效的NOP操作,讓整個流水線停頓一個或者多個周期。

 

4、操作數前推

    結構冒險:增加資源。

    數據冒險:流水線停頓。

    這兩種解決方法比較死板,還會造成資源的浪費。

    另一個解決方法:操作數前推。可以在第一條指令的執行階段完成之后,直接將結果數據傳輸給到下一條指令的 ALU。然后,下一條指令不需要再插入兩個 NOP 階段,就可以繼續正常走到執行階段。也就是在第 1 條指令的執行結果,直接“轉發”給了第 2 條指令的 ALU 作為輸入。不但可以單獨使用,還可以和流水線冒泡一起使用。

 

5、亂序執行

 在流水線里,后面的指令不依賴前面的指令,那就不用等待前面的指令執行,它完全可以先執行。

 

 可以看到,因為第三條指令並不依賴於前兩條指令的計算結果,所以在第二條指令等待第一條指令的訪存和寫回階段的時候,第三條指令就已經執行完成了。

    CPU實現亂序執行的方式:

 

    (1)在取指令和指令譯碼的時候,CPU還是在按順序工作

    (2)指令譯碼完成之后CPU進行一次指令分發,把指令發到一個叫作保留站(Reservation Stations)的地方。

    (3)這些指令不會立刻執行,而要等待它們所依賴的數據,傳遞給它們之后才會執行。

    (4)一旦指令依賴的數據來齊了,指令就可以交到后面的功能單元(Function Unit,FU),其實就是 ALU,去執行了。很多功能單元可以並行運行,但是不同的功能單元能夠支持執行的指令並不相同。

    (5)指令執行的階段完成之后,把結果存放到一個叫作重排序緩沖區(Re-Order Buffer,ROB)的地方。

    (6)在重排序緩沖區里,CPU 會按照取指令的順序,對指令的計算結果重新排序。只有排在前面的指令都已經完成了,才會提交指令,完成整個指令的運算結果。

    (7)實際的指令的計算結果數據,並不是直接寫到內存或者高速緩存里,而是先寫入存儲緩沖區(Store Buffer) ,最終才會寫入到高速緩存和內存里。

    

    在亂序執行的情況下,只有 CPU 內部指令的執行層面,只要我們能在指令的譯碼階段正確地分析出指令之間的數據依賴關系,這個“亂序”就只會在互相沒有影響的指令之間發生。指令的計算結果寫入到寄存器和內存之前,依然會進行一次排序,以確保所有指令在外部看來仍然是有序完成的。

 

6、控制冒險

 實現背景:增加資源、流水線停頓、操作數前推、亂序執行,這些解決各種“冒險”的技術方案,所有的流水線停頓操作都要從指令執行階段開始。取指令和指令譯碼不會需要遇到任何停頓,這是基於一個假設。這個假設就是,所有的指令代碼都是順序加載執行的。不過這個假設,在執行的代碼中,一旦遇到 if…else 這樣的條件分支,或者 for/while 循環,就會不成立。

 

    條件跳轉指令的本質:在 jmp 指令發生的時候,CPU 可能會跳轉去執行其他指令。jmp 后的那一條指令是否應該順序加載執行,在流水線里面進行取指令的時候,沒法知道。要等 jmp 指令執行完成,去更新了 PC 寄存器之后,才能知道,是否執行下一條指令,還是跳轉到另外一個內存地址,去取別的指令。跳轉指令的比較結果,仍然要在指令執行的時候才能知道。在流水線里,第一條指令進行指令譯碼的時鍾周期里,其實就要去取下一條指令了。這個時候,其實還沒有開始指令執行階段,自然也就不知道比較的結果。

  

    分支預測:

    就是讓 CPU 來猜一猜,條件跳轉后執行的指令,應該是哪一條。

  (1)假裝分支不發生,仍然按照順序,把指令往下執行。分支預測失敗,就把后面已經取出指令已經執行的部分,給丟棄掉。這個丟棄的操作,在流水線里面,叫作 Zap 或者 Flush。

 

 (2)動態分支預測

      一級分支預測:用一個比特,去記錄當前分支的比較情況,直接用當前分支的比較情況,來預測下一次分支時候的比較情況。

      狀態機:2 個比特來記錄對應的狀態。這樣這整個策略,就可以叫作 2 比特飽和計數,或者叫雙模態預測器(Bimodal Predictor),就是根據前面兩條挨着的分支判斷下面一條分支。

 

7、嵌套循環影響性能的實例

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)+ "ms");

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

 

  分析:

  這個差異就來自分支預測。循環其實也是利用 cmp 和 jle 這樣先比較后跳轉的指令來實現的。每一次循環都有一個 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 萬次預測錯誤。

    第一段代碼發生“分支預測”錯誤的情況比較少,更多的計算機指令,在流水線里順序運行下去了,而不需要把運行到一半的指令丟棄掉,再去重新加載新的指令執行。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM