圖解CPU為何要亂序執行


流水線執行

腦補 CPU 執行是這樣。

不過幾乎所有的馮·諾伊曼型計算機的CPU,其工作都可以分為 5 個階段:取指令、指令譯碼、執行指令、訪存取數、結果寫回。

1. 取指令階段

取指令(Instruction Fetch,IF)階段是將一條指令從主存中取到指令寄存器的過程。 程序計數器 PC 中的數值,用來指示當前指令在主存中的位置。

2. 指令譯碼階段

取出指令后,計算機立即進入指令譯碼(Instruction Decode,ID)階段。 在指令譯碼階段,指令譯碼器按照預定的指令格式,對取回的指令進行拆分和解釋,識別區分出不同的指令類別以及各種獲取操作數的方法。

3. 執行指令階段

在取指令和指令譯碼階段之后,接着進入執行指令(Execute,EX)階段。 此階段的任務是完成指令所規定的各種操作,具體實現指令的功能。

4. 訪存取數階段

根據指令需要,有可能要訪問主存,讀取操作數,這樣就進入了訪存取數(Memory,MEM)階段。

5. 結果寫回階段

作為最后一個階段,結果寫回(Writeback,WB)階段把執行指令階段的運行結果數據“寫回”到某種存儲形式:結果數據經常被寫到CPU內部寄存器中,以便被后續的指令快速地存取;在有些情況下, 結果數據也可被寫入相對較慢、但較廉價且容量較大的主存。許多指令還會改變程序狀態字寄存器中標志位 的狀態,這些標志位標識着不同的操作結果,可被用來影響程序的動作。

在指令執行完畢、結果數據寫回之后,若無意外事件(如結果溢出等)發生,計算機就接着從程序計數器PC中取得下一條指令地址,開始新一輪的循環,下一個指令周期將順序取出下一條指令。

指令重排

我們引現在CPU Nehalem 微架構如下,揭開它的神秘面紗。

乍一看不明覺厲,我們從出現頻率高的 μop | Micro Operation(micro-op)入手。它是類 RISC (精簡指令集或簡單指令集)處理器導致的一項設計,從 Pentium Pro 開始在 IA 架構出現。處理器接受的是 x86 指令(CISC 指令,復雜指令集),而在執行引擎內部執行的卻不是x86 指令。

RISC 架構的特點就是指令長度相等,執行時間恆定(通常為一個時鍾周期),因此處理器設計起來就很簡單,可以通過深長的流水線達到很高的頻率,IBM 的 Power6 就可以輕松地達到 4.7GHz 的起步頻率。和 RISC 相反,CISC 指令的長度不固定,執行時間也不固定,因此 Intel 的 RISC/CISC 混合處理器架構就要通過解碼器 將 x86 指令翻譯為 uop,從而獲得 RISC 架構的長處,提升內部執行效率。

x86 指令大部分簡單指令可以翻譯為一對一翻譯,復雜的可能 1 ~ 4 條 μops。解碼器是按位數取指的,在經過譯碼,因此每次可能產生多條 μops。計算機執行符合局部性原理,這里不僅指同個指令可能重復執行,也指內存訪問。而內存訪問顯然是比較慢的,對多條指令重新排序,把訪存相關的堆一起,顯然是可以提升效率的。

ROB(Re-Order Buffer,重排序緩沖區)是一個非常重要的部件,它是將亂序執行完畢的指令們按照程序編程的原始順序重新排序的一個隊列,以保證所有的指令都能夠邏輯上實現正確的因果關系。

並行執行

Nehalem 具備 6 個執行端口,每個執行端口具有多個不同的單元以執行不同的任務,然而同一時間只能有一條指令(uop)進入執行端口,因此也可以認為 Nehalem 有 6 個“執行單元”,在每個時鍾周期內可以執行最多 6 個操作(或者說,6 條指令)。

然而這些執行端口並不都是用於計算,實際上,有三個執行端口是專門用來執行內存相關的操作的,只有剩下的三個是計算端口。其余三個用來存取單元,一個用於所有的 Load 操作(地址和數據),一個用於 Store 地址,一個用於 Store 數據。

據統計 Load 操作占據了通常程序的 1/3 左右,並且 Load 操作可能會導致巨大的延遲(在命中的情況下,Nehalem 則為 4 個時鍾周期。L1 未命中時則會訪問 L2 緩存,一般為 10~12 個時鍾周期。訪問 L3 通常需要 30~40 個時鍾周期,訪問主內存則可以達到最多約 100 個時鍾周期)。所以CPU執行會盡可能使用已 Load 到寄存器的數據,一些編譯器優化也是如此操作,把一些經常使用的數值直接寫入寄存器,都是一個道理。

會出現啥問題?

亂序執行本身不會產生問題(寄存器重命名等),保證單個 CPU 執行快速高效。為啥還有 volatile 這個經典問題呢?別急,我們先總結下 CPU 為了加速執行引入的手段。

  • 指令細分,流水線執行
  • 多堆硬件,指令重排並行執行
  • 原地訪問,盡量不訪存
這里有意跳過超線程/同步多線程(HT/SMT)技術,因為 同步多線程(SMT,Simultaneous Multithreading)為額外線程也增加了對應的上下文晶體管,不會產生影響。

流水線執行

指令拆分之后,可能會分為多條指令,不是原子操作。那么問題就來了,比如多線程同時執行 i++ ,剛執行 100 條指令,突然線程切換了。等到回來繼續執行,假設執行到回寫,這就沖突了,不管丟棄還是強刷,結果肯定不對了。為了保證原子性操作,互斥資源可以上鎖,同時只讓一個 CPU 執行。

亂序執行

先看經典案例

// CPU0
void foo() {
    a = 1;
    b = 1;
}

// CPU1
void bar() {
    while (b == 0) continue;
    assert(a == 1);
}

我們分析下,在這個代碼塊中,CPU0 執行 foo 函數,CPU1 執行 bar 函數。但在對變量 a 和 b 進行賦值的時候,有兩種情況會導致它們的賦值順序被打亂。

  1. CPU 亂序執行,因為 a 和 b 是無關的
  2. 在寫回時,有可能 b 所對應的緩存行會先於 a 所對應的緩存行進入獨占狀態,也就是說 b 會先寫入緩存

這里有人會疑問,那 MESI 一致性協議干啥去了?

這是因為 MESI 在 Share 狀態下,如果一個核想獨占緩存進行修改,就需要先給所有 Share 狀態的同伴發出 Invalid 消息,等所有同伴確認並回復它“Invalid acknowledgement”以后,它才能把這塊緩存的狀態更改為 Modified,這是保持多核信息同步的必然要求。這個過程相對於直接在核內緩存里修改緩存內容,非常漫長。這也就會導致,某個核請求獨占時間比較長。因此操作系統並沒有嚴格遵守,而是提供內存屏障輕量級實現最終一致性。

原地訪問

都沒訪問內存,寄存器是 CPU 獨占的,MESI 又沒嚴格實現,另個 CPU 訪問過期數據管它啥事。

總結

CPU 為了加快執行引入了流水線、堆硬件、指令重排、並行執行、原地訪問等技術。而 MESI 很重量級,操作系統並未實現,提供了輕量級的內存屏障。引發了原子問題,以及並發問題,可以通過 Lock 和 volatile 解決。

題外

這樣是不是記得更牢了,遇到問題如何選擇也有依據了~


免責聲明!

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



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