下表列出了CPU關鍵技術的發展曆程以及代表系列,每一個關鍵技術的誕生都是環環相扣的,處理器這些技術發展曆程都圍繞着如何不讓“CPU閑下來”這一個核心目标展開。
關鍵技術
時間
描述
指令緩存(L1)
1982
預讀多條指令
資料緩存(L1)
1985
預讀一定長度的資料
流水線
1989
一條指令被拆分由多個單元協同處理, i486
多流水線
1993
多運算單元多流水線并行處理, 奔騰1
亂序+分支預測
1995
充分利用不同元件協同處理, 奔騰Pro
超線程
2002
引入多組前端部件共享執行引擎, 奔騰4
多核處理器
2006
取消超線程,降低時鐘頻率,改用多核心, Core酷睿
多核超線程
2008
重新引入超線程技術,iX系列

現代CPU上有很多個計算元件,有邏輯運算,算術運算,浮點運算,讀寫位址等,一條機器指令由多個元件共同協作完成,比如i486的五級流水線中,一條指令就被分解為如下幾個階段:取址,解碼,轉譯,執行,寫回。
一條CPU指令由多條機器指令組成,CPU的一個震蕩周期,是指這些基礎指令對應元件執行的周期,我們可以簡單地認為,一個時鐘周期的震蕩驅動處理器上的所有部件做一次操作,CPU的主頻一般就是指這震蕩(時鐘)周期的頻率。
現代計算機的主頻已經逐漸接近實體極限,主頻的大小受限于半導體工藝,流水線級數,功耗等一系列名額。
由于一條指令由多個元件協作執行,一條指令進入某一階段,則後面的元件都處于空閑狀态,指令在執行的過程中隻會有一個元件繁忙,其他元件都是空閑的,是以為了充分解放各個部件的使用,提高使用率,引入了流水線處理機制。
i486開始引入流水線的概念,把一條指令拆成了5個部分,當一個指令進入下一階段,CPU可以直接加在下一條指令,這樣所有的單元都被充分利用起來。
流水線級數越高,每個元件做的事情越少,時鐘周期也就可以做的越短,吞吐量就會越高,目前的主流處理器有高達14級流水線
但是這樣仍然會有一些CPU的空閑存在,被稱為<code>流水線氣泡</code>: 在流水線進行中出現部分元件空閑等待的情況
出現快慢不一的指令,元器件之間的執行速度不一樣
出現對元件利用不一樣的指令,比如算數計算和取地指令分别對應不同的處理單元
出現資料依賴的指令,比如第二條指令的輸入依賴第一條的輸出,或者條件判斷
為了解決流水線氣泡,CPU嘗試把後續的指令提前加載處理,充分解放,1995年引入了亂序執行政策,可以不按順序的執行指令。
亂序執行引入了 微指令(μOP) 的概念,由于操作粒度拆分更細,流水線被進一步更新為“超标量流水線”,高達12級,進一步提高了性能。
拆分後的 μOP 進入重排隊列,由排程器排程各個單元并行執行,每個單元隻要資料準備完畢即可開始處理。
但是亂序執行打破了分支判斷的有序性,也就是可以提前執行if else while等jmp相關指令的操作,而完全不等待判斷指令的傳回,CPU引入了一個分支判斷出錯則復原現場的機制,但是這個機制的代價是巨大的,要清空流水線重新取位址。
為了解決亂序執行的分支判斷復原帶來的性能損耗,引入了分支預測子產品,該子產品的工作原理和緩存很像,存儲一個分支判斷指令最近多次的判斷結果,當下次遇到該指令時候,預測出後續走向,改變取指階段的走向。
在引入引入亂序技術後,指令的準備階段(Front中的取指,轉義,分支判斷,解碼等)仍大于執行階段,導緻取指譯碼繁忙而邏輯運算單元空閑,于是CPU把這一部分拆成前端單元(Frontend),引入多前端的超線程技術。
超線程技術是建立在亂序執行技術上,多個前端翻譯好的 μOP一起進入重排Buffer, 共享ROB單元,進而共享執行引擎。
我們可以簡單地認為,Frontend決定邏輯核數量,Execution決定實體核數量
後續Inter引入多核心技術後,降低了CPU主頻縮短流水線,增加每個單元的工作,企圖通過多核心的優勢來簡化架構,短暫的取消了超線程技術(多個總比一個好),後續又重新加入,漸漸變成了現在常見的CPU的多核超線程的CPU架構。
L1 Cache,分為資料緩存和指令緩存,邏輯核獨占
L2 Cache,實體核獨占,邏輯核共享
L3 Cache,所有實體核共享
Cache Line可以簡單的了解為Cache之間最小換入換出單元,在64位機器上一般是64B,也就是用6位表達。通常寄存器從Cache上讀資料是以字為機關,Cache之間的更新是以Line為機關,即使隻寫一個字的資料,也要把整個Line寫回到記憶體中。
全映射: Cache直接映射到記憶體的一片區域,同一個line隻能映射在指定的位置,Cache失效率高
全關聯: Cache中所有的資料都是Line,Cache失效率低,但是查找速度慢
組關聯: 全映射和全關聯的折中方案,整體映射,局部關聯
在組關聯的Cache系統中,判斷一個Cache是否命中一般分為兩個階段,記憶體被分為三個部分
低6個位元組用于CacheLine内的尋址
高位一般劃分為tag和set兩部分,如果總Cache大小32K,8路的組關聯,則每一路是4K,則每一組是4K/64=64,則需要Set的大小是8個位元組
首先用set索引到對應的組,這一步是直接下标偏移
然後再用tag+line,在一組中輪訓比對每一路,這一步是循環周遊
是以一次Cache尋址通過一次偏移+一次周遊即可定位,組關聯通過調整路數來平衡Cache命中率和查詢效率
Cache一緻性是指,各個核心的Cache以及主記憶體的内容的一緻性,這一部分由CPU以一個有限狀态機通過單獨的總線通信保證。 英特爾使用協定是MESI協定。
MESI協定把Cache Line分解為四種狀态,在不同的Cache中,分别有這幾種狀态
Modified 資料有效,資料修改了,和記憶體中的資料不一緻,比主記憶體新,資料隻存在于目前Cache中。
Exclusiv 資料有效,資料和主記憶體一緻,資料隻存在于目前Cache中
Shared 資料有效,資料和主記憶體一緻,資料存在于多個核心中
Invalid 資料無效,應當被丢棄
同時對于導緻狀态出現遷移的的事件也分為四種,任何時
Local Read 本地讀取,本核心讀取本地,同時給其他核心發送 Remote Read事件
Local Write 本地寫如,本核心寫入資料,同時給其他核心發送 Remote Write事件
Remote Read 遠端讀取
Remote Write 遠端寫入
__E->M__: <code>Local Write</code> 事件觸發,本地獨占資料寫入後變成獨占且被修改
__M->E__: 不可能發生
__M->S__: <code>Remote Read</code> 事件觸發,由于其他核心要讀,是以要先寫入記憶體,一緻後變成多核心共享 Shared
__S->M__: <code>Local Write</code> 事件觸發,多核共享,寫入後變成獨占修改,同時給核心狀态變成 Invalid
__I->M__: <code>Local Write</code> 事件觸發,無效變成本地有效且獨占,同時其他核心變成 Invalid
__M->I__: <code>Remote Write</code> 事件觸發,其他核心寫入了資料,本地資料變成無效
__S->E__: 不可能發生
__E->S__: <code>Remote Read</code> 事件觸發,其他核心要讀,是以要先寫入記憶體,一緻後變成多核心共享 Shared
__E->I__: <code>Remote Write</code> 事件觸發,其他核心寫入了資料,本地資料變成無效
__I->E__: <code>Local Read</code> 事件觸發,且當其他核心沒有資料時
__I->S__: <code>Local Read</code> 事件觸發,且當其他核心資料有資料時
__S->I__: <code>Remote Write</code> 事件觸發,其他核心寫入了資料,本地資料變成無效
再談記憶體屏障 我們通常會把記憶體屏障了解成解決各個核之間的Cache不同步帶來的問題,其實CPU已經通過硬體級别解決了這個問題。 記憶體屏障主要解決的是編譯器的指令重排和處理器的亂序之行帶來的不可預測性,告訴編譯器不要在再屏障前後打亂指令,同時也告訴處理器在屏障前後不要亂序執行。
ALU處理單元數量
分支預測成功率
充分利用流水線處理
高速緩存命中率
測試說明:代碼中 v_1 v_2 中的數字代表循環内展開的數量,比如
mov_v_1
mov_v_2
同時這些測試是為了驗證CPU的,是以為了防止編譯器優化加了很多迷惑的判斷和标記,所有測試資料來自本地開發機測試。
當把循環展開(總mov次數不變),我們可以看到執行時間會迅速縮短
Case
Time
Desc
15.8 (3)
8.5 (1.5)
去除循環開銷,流水線提高運算效率
mov_v_3
5.4 (1)
兩個運算單元可以并行處理
mov_v_4
4.4 (0.75)
mov_v_8
4.1 (0.375)
mov_v_10
3.6 (0.3 )
mov_v_100
3.3 (0.03)
和v_10 差距接近于循環的開銷
通過 v_10 和 v_100 我們可以簡單計算出,一次jmp的開銷在 0.3 nm 左右
add_v_1
16.7
add_v_10
17.0
10倍循環展開,資料有依賴無法流水線并行執行
add_v_10_2
8.6
兩個互不幹擾的加法,兩個運算單元可以并行處理
add_v_10_3
8.3
三個互不幹擾的加法,隻有兩個運算單元達到上限
這個例子中,第二次的 "a=a+b" 必須依賴第一次傳回,前後有資料依賴無法充分利用流水線最大化并行處理,當引入第二個a0後,兩個加法互不影響,則計算速度接近翻倍。
mul_v_1
23.9
乘法的指令周期大于加法指令周期
mul_v_10
24.0
10倍循環展開,資料有依賴無法并行執行
mul_v_10_2
12.7
兩個互不幹擾的乘法,隻有兩個運算單元達到上限
編譯器可以通過位移操作+ADD優化乘法 無符号常量的除法可以等價于乘法+位移,但是對于變量必須用DIV運算符
if_p_sort
0.21
if_p_nosort
0.71
預測相同指令的結果
随機數組會導緻 if 判斷的預測變得不可預知,通過對數組重排,可以讓CPU分支預測命中率達到100%,進而大幅度的減少流水線回退機制,提高亂序執行的吞吐量能力。
第一個例子預設命中if分支
第二個例子,預設命中else分支
if_t
26
分支預讀無跳轉
if_f
29
分支預讀跳轉,中斷重新加載
if else判斷會被彙編成 jle 等指令,if的部分緊挨着 jle指令,else的部分被放在後面,是以當 CPU預讀指令亂序執行,首先會預執行jle後續的指令,當判斷生效後再決定是否清除現場跳轉到else 是以在執行效率上,隻命中 else 的指令會比隻命中 if 的指令慢一點。 C++中的 LIKELY / UNLIKELY 針對于此優化,通過把高命中率的分支上提到 jmp 附近。 本測試中兩者分支預測總是正确但仍然有不小的性能差異,我推測是因為分支預測成功後幹擾指令的讀取順序,這一部分本身相比較直接順序讀取也是有開銷的。
這個測試通過循環周遊一個大數組來測試CacheLine邊界對性能的影響,每一次周遊的步長從一個CPU字長(8Byte)開始,然後翻倍,64B,128B,256B... 等等,然後循環次數減半。當步長在一個CacheLine中,在這個CacheLine内部的循環會全部命中Cache,跨域Line後才會去下一級Cache中或者記憶體中讀取資料。
我們看到當K=8 到 K=16 時,即使循環減半耗時反而增加,因為K=8時剛好是一個CacheLine的邊界,當跨域這個邊界,每次循環都要跨越一級(沒命中的話)去下一級Cache中load資料。
寄存器操作以及算術邏輯運算開銷小于尋址開銷
算術計算性能很高,2的乘法以及除法效率很高,常量的除法效率高于變量(編譯器優化)
可以通過多線程利用多核,同樣也可以多次利用單核心的多算術邏輯單元,隻是通常收益不是很大
CPU總是會流水線預讀指令,但是資料依賴以及條件跳轉會産生流水線氣泡
分支復原代價很大,LIKELY操作把相關的分支上提,增加指令預讀流水線的效率
分支預測可以有效緩解亂序執行下的流水線氣泡,有序的if判斷可以增加分支預測命中率
局部性原理,資料對齊的重要性,合理調整 Cache Line 的邊界,超過64B會出現兩次cache line的讀取
局部性原理不僅适用于資料,同樣适用于指令,熱點for循環内的代碼了不易過大(超越L1cache),但也不宜過小(無法充分利用流水線)
cpu指令周期本身很短,大部分時間隻需要關注熱點部分的代碼
熔斷利用現代作業系統的亂序執行的漏洞,亂序會執行到一些非法的代碼,但是系統中斷需要時間,資料可能已經讀入Cache,在通過把資料轉換為探測Cache的讀寫速度來确定資料内容。
Meltdown涉及到上述CPU幾個特性, 利用熔斷原理,可以通路核心空間上的位址,也就是可以通路任意實體位址,這就代表可以跨程序的非法通路别的程序中的資料。
申請一塊大記憶體,用于做探測記憶體
注冊SIGSEGV異常處理函數,處理非法通路後的探測工作
構造一段代碼,通路非法的實體位址
把通路後的實體位址投射到探測記憶體上,由于CPU的指令預讀,非法通路中斷發生前已經運作了後續指令
中斷回調中,周遊探測記憶體上的資料,找到通路最快的一個點,進行權重
多次循環上述探測過程,計算出得分最高的那個點,其偏移量就是所要的值
熔斷的探測代碼如下:
熔斷的核心代碼如下,也就是上面的 MELTDOWN 宏 :
使用者可以通過 /proc/pid/pagemap 找到目前程序邏輯位址對應的實體位址,然後再通過核心的高端記憶體映射,把實體位址轉換成核心用的邏輯位址,核心的邏輯位址的分頁在核心段,位址也是核心位址,這部分使用者本來是沒權限通路的,通過熔斷可以探測這部分位址的内容。
參考文獻:
分支預測測試
<a href="https://stackoverflow.com/questions/11227809/why-is-it-faster-to-process-a-sorted-array-than-an-unsorted-array">https://stackoverflow.com/questions/11227809/why-is-it-faster-to-process-a-sorted-array-than-an-unsorted-array</a>