現代中央處理器(CPU)是怎樣進行分支預測的?
尊重原創版權: https://www.gewuweb.com/hot/16659.html
現代中央處理器(CPU)是怎樣進行分支預測的?
人們一直追求CPU分支預測的準確率,論文Simultaneous Subordinate Microthreading
(SSMT)中給了一組資料,如果分支預測的準確率是100%,大多數應用的IPC會提高2倍左右。
為了比較不同分支預測演算法的準確率,有個專門的比賽:Championship Branch Prediction(CPB)。CPB-5的冠軍是TAGE-
SC-L,在TAGE-SC-L Branch Predictors Again中有詳細描述:
但是分支預測準確率高意味著更復雜的演算法,佔用更多的矽片面積和更多的功耗,同時還會影響CPU的週期時間。更不幸的是,不同程式呈現不同的特性,很難找到一種放之四海而皆準的分支預測演算法。一種演算法可能對一類程式有很高的準確率,但是對另一類程式的效果卻不盡然。
另外現代計算機都是Superscalar架構,取指階段會同時取出多條指令(fetch
group),這需要對取出的指令全部進行分支預測。而且這種情況下的misprediction penalty是M * N,預測失敗的無用功更多。(M =
fetch group內指令數, N = branch resolution latency,也就是決定分支最終是否跳轉需要多少週期)
分支預測大方向包含四個主題:
- 在取指階段指定是不是分支,在Multiple-Issue Superscalar背景下複雜很多。
- 如果指令是分支指令,判斷該指令taken or not taken(跳轉 or 不跳轉)。這裡有些特殊的是無條件執行的分支指令,例如call/jump。
- 如果分支指令跳轉,預測該分支的目標地址 —— Branch Target Address,這裡分為PC-relative直接跳轉(direct)和Absolute間接跳轉(indirect)。
- 如果預測失敗,需要有恢復機制,不能執行wrong path上的指令。
分支預測是影響CPU效能的關鍵因素之一,需要在硬體消耗、預測準確度和延遲之間找到一個平衡點。為了理解分支預測演算法本身,假設CPU每週期只取一條指令。
先說下相對簡單的目標地址預測
目標地址預測
分支的目標地址BTA(Branch Target Address)分為兩種:
- 直接跳轉(PC-relative, direct) : offset以立即數形式固定在指令中,所以目標地址也是固定的。
- 間接跳轉(absolute, indirect):目標地址來自通用暫存器,而暫存器的值不固定。CALL和Return一般使用這種跳轉。
直接跳轉
使用BTB(Branch Target
Buffer)記錄目標地址,相當於一個Cache。為了節省資源,BTB一般只記錄發生跳轉的分支指令目標地址,不發生跳轉的目標地址其實就是順序取指令地址。
下面是最簡單的一個BTB結構:
BTB還有其他變種:
- 如果BTB容量有限,BTB entry中需要有PC值的一部分作為tag。
- 在上面的基礎上,做組相連結構,不過一般way的個數比較小。
- 為了快速識別分支型別,還可以存分支是call、return還是其他。
- BTB中存放t具體的target instruction,不存放BTA,流水線不需要再去額外取指令。這樣可以做branch folding優化,效果對unconditional branches尤其明顯。
間接跳轉
間接跳轉一般用在switch-case指令實現(類似jmpq *rax,rax,rax是case對應label地址)、C++這種虛擬函式呼叫等。
如果間接跳轉是用來呼叫函式,那麼目標地址還是固定的,用BTB就可以來預測。但如果是switch-
case這種,雖然分支指令地址是相同的,但是目標地址不固定,如果還是用BTB,準確率只有50%左右。很多CPU針對間接跳轉都有單獨的預測器,比如的Intel的論文The
Intel Pentium M Processor: Microarchitecture and Performance,其中介紹了Indirect
Branch Predictor:額外引入context-information——Global Branch History:
而對於return這種間接跳轉來說,目標地址同樣不固定,因為可能多有個caller呼叫同一個函式。CPU一般採用RAS(Return Address
Stack),將CALL指令的下一條指令地址寫入RAS,此時該地址在棧頂,下次return時可以直接從棧頂pop該地址用作return的目標地址。
對於遞迴的程式來說,RAS儲存的都是一個重複值,所以有的處理器會給RAS配一個計數器。如果將寫入到RAS的地址和上一次寫入的相同,說明執行的是一個CALL指令,直接將RAS對應的entry加1即可,保持棧頂指標不變,這相當於變相增加了RAS的容量。
一個8-entry的RAS可以達到95%的準確率。
分支方向預測
分成靜態預測和動態預測兩種:
- 靜態預測,Static prediction
- Always taken
- Predict branch with certain operation codes
- BTFN (Backward taken, forward not taken)
- Profile-based
- Program-based
- Programmer-based
- 動態預測,Dynamic prediction
- Last time prediction (single-bit)
- Two-bit counter based prediction (bimodal prediction)
- Two Level Local Branch Prediction (Adaptive two-level prediction)
- Two Level Global Branch Prediction (Correlating Branch Prediction)
- Hybrid Branch Prediction (Alloyed prediction)
- Perceptrons Prediction
- TAgged GEometric History Length Branch Prediction (TAGE)
先看靜態預測,1981年的論文A STUDY OF BRANCH PREDICTION STRATEGIES 對前三種靜態預測的準確率做過描述和準確率統計。
靜態預測
Always taken
全部跳轉,準確率在不同程式上差別很大。Always taken實現成本極低,其他分支預測演算法的準確率都應該比這個高,否則還不如用這個策略。
Predict branch with certain operation codes
該策略應用在IBM System 360/370上,是Always taken的一個改進版,根據分支指令的operation
code來覺得是否taken,比如<0、==、>=預測跳轉,其他不跳轉。
和Always taken結果對比來看, GIBSON的預測準確率從65.4%提高到了98.5%,唯一下降的是SINCOS,論文裡給的理由是branch
if plus被預測成了不跳轉,否則準確率會和Always taken策略下的準確率持平。
BTFN (Backward taken, Forward not taken)
這個演算法策略主要受到loop迴圈的啟發。當迴圈跳轉時,target address都是在當前PC值的前面(backward),迴圈結束時target
address在PC值的後面(forward)。
但是這個策略的準確率在不同程式表現上差別很大,SINCOS上的預測準確率甚至降到了35.2%。而且該策略還有個缺點:需要計算target
address並和當前PC值進行比較才能預測下一個PC值,這會比其他策略多消耗時鐘週期,預測失敗後的recovery成本也較高。
Profile-based
利用編譯器收集執行時的資訊(Profile-guided optimization,
PGO)來決定分支是否跳轉,編譯後的程式碼會插入額外的命令統計實際執行時的情況,比如GNU C++編譯器的Compiler Profiling,
-fbranch-probabilities。需要一個compiler-profile-compier的過程。
下圖是在SPEC92 benchmark下,採用這種策略的不同程式分支預測結果:
Program-based
論文Branch prediction for free詳細介紹了Program-based方式的分支預測。例如:
- opcode heuristic : 將BLEZ預測為not taken等。(負數在很多程式碼裡代表error values)
- loop heuristic : 與BTFN一樣。
- Pointer comparisons
這個方法優點是不需要額外的profiling,缺點是Heuristic也可能沒有代表性。該方法的平均預測失敗率在20%左右。
Programmer-based
由實際寫程式碼的人來給出分支是否跳轉的資訊。這個不需要提前的profiling和analysis,但是需要程式語言、編譯器和ISA的支援。
比如gcc/clang的__builtin_expect和C++20新增的likely, unlikely , 這也更利於編譯器對程式碼結構進行排列。
constexpr double pow(double x, long long n) noexcept {
if (n > 0) [[likely]]
return x * pow(x, n - 1);
else [[unlikely]]
return 1;
}
靜態預測的優點是功耗小,實現成本低,但是最大的缺點是無法根據動態的程式輸入變化而改變預測結果,dynamic compiler(JAVA
JIT、Microsoft CLR)會在一定程式上改善這個缺點,但也有runtime overhead。所以現代CPU通常採取動態預測的方式。
動態預測
動態預測的優點是可以根據分支歷史輸入和結果進行動態調整,且不需要static profiling,缺點是需要額外的硬體支援,延遲也會更大。
Last time prediction (single-bit)
快取上一次分支的預測結果。缺點是隻有單位元,無容錯機制。比如分支歷史是TNTNTNTNTNTNTNTNTNTN,此種演算法的準確率是0%。
Two-bit counter based prediction (bimodal prediction)
該演算法不會馬上利用前一次的預測結果,而是【前兩次】,核心是分支指令偶爾發生一次方向改變,預測值不會立刻跟著變。
每個分支對應4個狀態:Strongly taken、Weakly taken、Weakly not taken、Strongly not
taken。初始狀態可以設為Strongly not taken或者Weakly not taken.
從上圖可以看出來,之所以叫【飽和】,是因為計數器在最大值和最小值時達到飽和狀態,如果分支方向保持不變,也不會改變計數器狀態。更一般的,有n位飽和計數器,當計數器的值大於等於(2^n
- 1)/2時,分支預測跳轉,否則不跳轉。n一般最多不超過4,再大也沒有用。
由於不可能為每個分支都分配一個2位飽和計數器,一般使用指令地址的一部分去定址Pattern History
Table(PHT),PHT每一個entry存放著對應分支的2位飽和計數器,PHT的大小是2^k *
2bit。但是這種方式定址,會出現aliasing的問題,如果兩個分支PC的k部分相同,會定址到同一個PHT
entry。如果兩個分支的跳轉方向相同還好,不會互相干擾,這種情況叫neutral aliasing。但如果兩個分支跳轉方向不相同,那麼PHT
entry的計數器會一直無法處於飽和狀態,這種叫negative aliasing。後文說到其他分支預測方法怎麼緩解這個問題的。
PHT的大小也會直接影響分支預測的準確率。論文Combining Branch
Predictors有一組測試資料,隨著PHT增大,預測準確率在93.5%左右達到飽和。當PHT是2KB時,用分支地址中的k=log(210248b/2b)=13位去定址PHT.
飽和計數器的更新
有3個時機供選擇:
- 分支預測後。
- 分支結果在流水線執行階段被實際計算後。
- Commit/Retire階段。
第一個方案雖然更新快,但是結果不可靠,因為預測結果可能是錯誤的。
第二個方案的更新階段比第三個早,但是最大的缺點是當前分支有可能處於mis-
prediction路徑上,也就是之前有分支指令預測失敗了,會導致它從流水線中被flush,對應的技術器不應該被更新。
第三個方案的缺點是從預測到最終更新計數器的時間過長,導致該指令可能在這期間已經被取過很多次了,例如for迴圈體很短的程式碼,這條指令在進行後面分支預測的時候,並沒有利用之前執行的結果,但是考慮到飽和計數器的特點,只要計數器處於飽和狀態,預測值就會固定,即使更新時間晚一點,也不會改變計數器狀態,所以並不會對預測器的準確率產生很大的負面影響,該方案是一般採取的方案。
該演算法的評價準確率在85%-90%之間。下圖是採用SPEC89 benchmark,4096-entry 2-bit
PHT的預測準確率情況。20世紀90年代很多處理器採用這種方法,比如MIPS R10K,PHT=1Kb、IBM PowerPC 620,PHT=4Kb。
但是對於TNTNTNTNTNTNTNTNTNTN這種極端情況,準確率是50%(假設初始計數器的狀態是weakly taken),還是不可接受。
Two Level Local Branch Prediction (Adaptive two-level prediction)
1991年的論文Two-Level Adaptive Training Branch
Prediction提出可以基於分支歷史結果進行預測,每個分支對應一個Branch History Register(BHR),每次將分支結果移入BHR。
從上圖可以看出為什麼是Two-level :
- First level : 一組位寬為n位的BHR,記錄分支過去n次的結果,使用分支PC定址。所有BHR組合在一起稱為Branch History Register Table(BHRT or BHT)。
- Second level : 使用BHR去定址PHT,大小為2^n * 2bit,PHT每個entry儲存對應BHR的2位飽和計數器。
假如分支結果是TNTNTNTNTNTNTNTNTNTN,BHR是2位,那就需要一個有著4個entry的PHT。BHR的值交替出現10和01。當BHR的值是10時,對應PHT的第2個entry,因為下次分支肯定是跳轉,所以該entry對應的飽和計數器是Strongly
taken狀態。當分支需要進行預測時,發現BHR=10,會預測下次跳轉。
再比如論文Combining Branch Predictors中的例子,一個道理,不詳細解釋了。
該論文測試對比了在不通過PHT大小下Local History Predictor和Bimodal
Predictor的準確率。隨著PHT體積增大,Local History Predictor的準確率維持在97%左右。
該演算法還有2個需要權衡的地方:
- BHR的位寬大小 : PHT entry的計數器達到飽和狀態的時間稱為training time. 在這段時間內,分支預測的準確率較低,而training time取決於BHR的位寬。【位寬過大】需要更多的時間,而且增加PHT體積,但是也會記錄更多的歷史資訊,提高預測準確度。但是【位寬過小】會不能記錄分支的所有結果,小於迴圈週期,使用2位的BHR去預測NNNTNNNTNNNT,PHT entry的計數器無法達到飽和狀態,預測結果就差強人意。BHR位寬需要實際進行權衡和折中。
- Aliasing/Interference in the PHT : 與2-bit counter演算法一個問題,下文詳述。
Two Level Global Branch Prediction(Correlating Branch Prediction)
該演算法思想是一個分支的結果會跟前面的分支結果有關。
下面的例子,如果b1和b2都執行了,那麼b3肯定不會執行。只依靠b3的local branch history無法發現這個規律。
if (2 == aa) { /** b1 **/
aa = 0;
}
if (2 == bb) { /** b2 **/
bb = 0
}
if (aa != bb) { /** b3 **/
...
}
local branch history使用BHR記錄。類似的,如果要記錄程式中所有分支指令在過去的執行結果,需要一個位寬為n的Global History
Register(GHR),再去定址對應PHT中的飽和計數器,整體結構跟Two Level Local Branch
Prediction很像。下圖的HR既可以指BHR,也可以指GHR。
再比如論文Combining Branch Predictors中的例子,常見的兩層for迴圈,GHR的0是內層迴圈j=3 not taken的結果:
BHR/GHR更新時機
同樣是三種選擇:
- 分支預測後。
- 分支結果在流水線執行階段被實際計算後。
- Commit/Retire階段。
Global Branch
Prediction中GHR更新一般是在分支預測後直接更新(speculative)。因為GHR記錄的全域性分支歷史,在現代CPU都是Superscalar架構的背景下,如果在分支retire後或者流水線分支結果實際被計算出來之後再更新,該指令後來的很多分支指令可能已經進入流水線中,他們都使用相同的GHR,這就違背了GHR的作用。如果當前指令預測失敗,後續指令都使用了錯誤的GHR,其實也沒有關係,因為反正他們隨後都會從流水線中被flush掉。但是需要一種機制對GHR進行recover,這是另外一個話題。
Local Branch Prediction中的BHR跟GHR有所不同,是在指令retire階段更新(non-
speculative),由於記錄的是local branch
history,只有在for迴圈體很短的情況,才可能出現一條指令在流水線提交階段更新BHR時,流水線內又出現了這條分支使用BHR進行預測的情況。但這種情況不會對預測效能有太大影響,因為經過warmup/training之後,預測器會預測for迴圈一直跳轉。而其他兩個階段更新需要recover機制,收益也不明顯。
GHR/BHR對應的PHT飽和計數器還是在指令retire階段更新,理由跟two-bit counter一樣。
Aliasing/Interference in the PHT
不同分支使用相同PHT entry,最壞情況導致negative aliasing:
可以通過下面的方法有效緩解:
- 增大PHT體積
使用更多的PC bit去定址,但也需要兼顧成本。
- 過濾biased branch
很多分支在99%的情況下跳轉方向是一致的,論文Branch classification: a new mechanism for improving
branch predictor performance提出可以檢測具有這些特點的分支,使用簡單的last-time或者static
prediction方法去預測,防止它們汙染其他分支結果。
- Hashing/index-randomization
最典型的還是McFarling在Combining Branch Predictors裡提出的Gshare,增加額外的context
information,將GHR與PC進行XOR hash去定址PHT:
再比如Gskew以及其各種變種,核心思想是使用多個hash方式和PHT,最後使用majority vote進行決策:
- Agree prediction
論文The Agree Predictor: A Mechanism for Reducing Negative Branch History
Interference提出了agree predictior,從論文名字也可以看出來是為了解決PHT的aliasing問題。
左邊是上面提過的Gshare結構,區別在右邊:
- 基於大多數分支總是傾向於一個跳轉方向,每個分支在BTB中增加一個bias bit,代表該分支傾向於taken還是not taken。
- PHT中還是2位飽和計數器,但是更新邏輯不同:當最終跳轉,結果和bias bit相同(agree)時,計數器自增,否則自減(disagree)。
如果選擇了正確的bias bit,即使兩個分支定址到一個PHT entry,那計數器也會一直自增到飽和,即agree狀態。
假如兩個分支br1和br2,taken的概率分別是85%和15%,且使用同一個PHT
entry,按照傳統的其他方案,兩個分支跳轉結果不同,更新計數器造成negative aliasing的概率是(br1taken, br2nottaken)
- (br1nottaken,br2taken) = (85% * 85%) + (15% * 15%) = 74.5%。如果是agree
predictor,br1和br2的bias bit分別被設定為taken和not taken,negative
aliasing的概率是(br1agrees, br2disagrees) + (br1disagrees, br2agrees) = (85% *
15%) + (15% * 85%) = 25.5%,大大降低了negative aliasing概率。
除了上面的解決方案,還有The bi-mode branch predictor提出的bi-mode,mostly-taken和mostly-not-
taken使用分開的PHT,降低negative aliasing、The YAGS Branch Prediction Scheme提出的YAGS預測器。
Hybrid Branch Prediction (Alloyed prediction)
基於Global history的演算法不能很好預測單個分支TNTNTNTNTN的情況,有些分支適合使用Local history去預測,有些適合Global
history,還有些只要Two-bit counter就夠了。Hybrid branch
prediction思想是使用多個分支預測器,然後去選擇最好最適合該分支的。
該方法的優點除了上面說的,還可以減少預測器整體的warmup/training
time。warmup快的預測器可以在開始階段優先被使用。缺點是需要額外新增一個meta-predictor去選擇使用哪個預測器。
最經典的應該是Alpha 21264 Tournament Predictor,對應的論文The Alpha 21264 Microprocessor
Architecture也是經典中的經典。
左邊是local branch prediction,兩級結構。第一級local history table大小是1024 *
10bit,也就是使用了10位寬的BHR,整個表格記錄1024條分支歷史資訊。10位寬的BHR需要PHT支援1024個飽和計數器,Alpha
21264採用的是3位飽和計數器。
右邊是global branch
prediction,12位寬的GHR,記錄全域性過去12條分支的歷史結果,PHT需要支援4096個飽和計數器,這裡使用的2位飽和計數器,和local那裡不一樣。
至於meta-predictor就是圖中標示的Choice
Prediction,使用GHR定址,所以有4096項,使用2位飽和計數器來追蹤兩個不同預測器的準確度,比如00代表local branch
prediction準確率更高,11代表global branch prediction更好。
而各個預測器的更新跟之前說過的時機一樣,BHR在指令retire更新,GHR在指令得到預測結果後更新,PHT中的飽和計數器在指令retire時更新。
Loop Prediction
當分支是loop迴圈時,一般最後一次是預測失敗的。Loop Predictor探測分支是否具有loop behavior並對該loop進行預測,loop
behavior定義為朝一個方向taken一定次數,再朝相反方向taken一次,對每次迴圈都是相同次數的預測效果很好。現在很多分支預測器都集成了loop
predictor,包括開頭介紹的TAGE-SC-L。
下面的圖參考Intel的論文The Intel Pentium M Processor: Microarchitecture and
Performance:
Perceptrons Prediction
2001年的論文Dynamic Branch Prediction with
Perceptrons提出完全不同的思想,採用機器學習的Perceptron去進行分支預測,這也是第一次將機器學習成功應用到計算機硬體上,雖然用到的方法很簡單。
Perceptron學習xi和最終結果之間的linear function,每個分支對應一個Perceptron。w0是bias
weight,若某個分支具有很強的傾向性,w0會對最終結果有很大的影響。xi是前面說過的GHR中對應的bit,只有0和1兩種取值,x0恆等於1。wi是對應的weight。若y
0,預測分支跳轉,否則不跳轉。
下圖是wi的調整過程。如果預測錯誤或者Perceptron的輸出小於等於某個閾值,就對wi進行調整。因為t、xi的取值只有1和-1,如果xi的取值和最終分支結果相同,對應的wi就+1,否則-1。一旦某個xi和分支結果有強關聯,對應的wi絕對值就會很大,對最終結果有決定性影響。w0比較特殊,x0恆等於1,w0跟t的值變化,如果t一直是1或者-1,那麼w0絕對值也會很大,決定最終結果。
下圖是整體流程,多了根據指令地址取對應Perceptron放到vector register的過程。
該演算法的優點是可以使用位寬更大的GHR。在上面介紹的其他演算法裡,PHT的entry數目是2^(GHR位寬),但該演算法僅僅為每一個分支分配一個Perceptron,增長是線性的。論文給出的演算法預測準確率也很可觀,感興趣的可以去論文裡看看。
演算法缺點是隻能學習linearly-separable
function,不能學習比如XOR等更復雜的關係,防止分支預測期處理過複雜的模型而導致critical path延時變大:
if(condition1) {}
if(condition2) {}
if(condition1 ^ condition2) {}
後續還有很多人優化Perceptrons Prediction,比如2011年的論文An Optimized Scaled Neural Branch
Predictor,2020年關於Samsung Exynos CPU Microarchitecture系列的論文Evolution of the
Samsung Exynos CPU Microarchitecture,後者也是Perceptron Prediction應用在現代CPU上的典型案例。
TAgged GEometric History Length Branch Prediction (TAGE)
2006年的論文A case for (partially) tagged Geometric History Length Branch
Prediction提出了TAGE prediction,分類屬於PPM-based
prediction,此後TAGE及其變種連續蟬聯CBP比賽的冠軍,包括開頭介紹的TAGE-SC-L。很多現代CPU都使用了基於TAGE思想的分支預測器。
有幾個關鍵點讓TAGE脫穎而出:
- TA:Tagged,新增TAG資料。不同分支指令hash到同一個PHT entry可能會導致negative aliasing。TAGE採用partial tagging,為了防碰撞,生成TAG的hash和上一步定址Tagged Predictor的hash函式也不同。
- GE:Geometric,將預測器分為一個2-bit Base Predictor和多個Tagged Predictor。
- Tagged Predictor使用PC和GHR[0:i]進行hash去定址。i可以是一個等比數列a0∗q^(n−1) ,比如GHR[0:5],GHR[0:10],GHR[0:20],GHR[0:40]。這麼做的目的是不同的分支需要不同長度的歷史結果才能達到更好的準確率,而之前的演算法使用相同位寬的GHR去定址PHT。
- 最終預測採用對應GHR最長的那個Tagged Predictor給出的結果,如果都沒有match上,使用一直能match的Base Predictor。
- 增加2-bit的useful counter,代表該預測值在過去是否對結果起到過正向作用。
Tagged Predictor的更新邏輯以及TAGE各種實驗資料,可以看看論文,寫的很詳細,限於篇幅這裡不列舉了:
至於分支預測剩下兩個主題那就更大更復雜了,感興趣的可以參考《超標量處理器設計》4.4 - 4.5節,或者Onur
Mutlu在Youtube上的Digital Design and Computer Architecture課程。
e上的Digital Design and Computer Architecture課程。