Pipeline的優點
現代微處理器的pipeline中包含許多階段,粗略地可以分成fetch、decode、execution、retirement,細分開來可以分成十多甚至二十多個階段。在處理器處理指令時,可以像流水線一樣同時處理位於不同階段的指令。
下圖,假設一個pipeline分為四個階段,每個階段耗費一個時鍾周期。
4條指令按照先后順序進入pipeline,每間隔一個時鍾周期,指令就能從pipeline的上一個階段轉移到下一個階段,在第四個時鍾周期時,4條指令全部進入pipeline內,各個階段都含有一條指令。按照這種策略,最佳的情況就是指令源源不斷地進入pipeline,pipeline中就會一直都在同時處理四條指令,那么指令的處理效率就是原來的4倍。
Pipeline在Branch上所面臨的問題
由於指令之間存在依賴關系,因此需要采用各種輔助機制來保證指令的流暢執行。我們之前就已經討論過指令間的源/目標操作數依賴,pipeline中是用in-flight機制來加速指令處理,這里討論另外一種依賴,就是分支(branch)對比較結果(flag)的依賴。指令中經常會出現跳轉指令,特別是條件跳轉指令,在得到條件的結果前,我們是不知道接下來會走哪個分支的,因此按照一般的邏輯,應該需要先等待比較結果執行完畢,再根據結構去取相應的分支進入pipeline內處理。不過這會導致指令的執行效率下降,因為在等待比較指令執行完成的過程中,后續的指令無法進入pipeline,也就是執行時間幾乎延遲了一整個個pipeline的時鍾周期。
在現代微處理器中,由於pipeline的細分,長度(階段)達到十多甚至二十多,因此如果不采取相應措施則會導致出現10~20個時鍾周期的延遲。
Branch Prediction
為了克服上述問題,pipeline中引入了Branch Prediction機制。Branch Prediction就是通過預測,把接下來最有可能執行的分支獲取進入pipeline,就像不存在對比較結果的依賴那樣直接執行,這么一來就保持了指令的流暢執行,這也被稱為Speculative Execution。不過這種通過預測獲取進入pipeline的分支終究只是預測分支,實際上不一定是執行這一分支,因此這部分指令的執行結果不應該從pipeline中輸出,即不應該執行retirement這一步驟。在得到比較結果后,就能知道預測的分支是否為實際應該執行的分支,如果是,pipeline中的預測分支指令就能繼續執行下去,否則就需要把預測分支的指令排空,重新獲取正確分支的指令進入pipeline繼續執行。
采用Branch Prediction機制后,條件跳轉的延遲將取決於預測的成功率。成功率越高,則能保證指令流暢執行,提升指令處理效率;成功率低,則會導致Branch Misprediction經常發生,這需要把預測分支排空,重新獲取指令執行,因此會降低指令處理效率。
Branch Predictor
我們把進行分支預測的硬件稱為Branch Predictor,也稱之為Branch Prediction Unit(BPU)。如前文所述,BPU的主要作用是預測接下來執行的指令分支,也就是說BPU作用於pipeline的前端(front-end)。
如上圖為一個NetBurst(Pentium 4)微處理器的粗略pipeline。如果所預測的分支還沒進入pipeline內,則需要從cache中讀取,BPU會在Fetch階段去控制讀取所預測的指令分支。所預測的分支也有可能已經在pipeline內,如以前執行過該分支,而該分支的指令在被解碼成μops后會存儲在Trace Cache,BPU可以通過控制Trace Cache向EU發送預測分支的μops。在確定了實際上所走的分支之后,retirement會向BPU進行反饋,更新BPU中的信息並用於下一次分支預測。
Implementation
Branch Prediction早在1950s末就引入了到了IBM Stretch處理器中,經過幾十年的發展,Branch Prediction進化出了多種實現方式。現代的處理器中往往包含其中的多個實現,以應用在不同的指令環境。。
Static Branch Prediction
Static Branch Prediction是最簡單的分支預測,因為它不依賴於歷史的分支選擇。Static Branch Prediction可以細分為三類:
- Early Static Branch Prediction,總是預測接下來的指令不走跳轉分支,即執行位於跳轉指令前方相鄰(比當前指令晚執行)的指令。
- Advanced Static Branch Prediction,如果所跳轉的目標地址位於跳轉指令的前方(比當前指令晚執行),則不跳轉;如果所跳轉的目標地址位圖跳轉指令的后方(比當前指令早執行)則跳轉。這種方法可以很有效地應用在循環的跳轉中。
- Hints Static Branch Prediction,可以在指令中插入提示,用於指示是否進行跳轉。x86架構中只有Pentium 4用過這種預測方式。
目前的Intel處理器會在缺少歷史分支信息的時候采用Advanced Static Branch Prediction來進行分支預測,也就是說如果某分支在第一次執行時會采用該預測方式。因此我們在進行編碼是需要進行注意,以便優化代碼的執行效率。
//Forward condition branches not taken (fall through) IF<condition> {.... ↓ } //Backward conditional branches are taken LOOP {... ↑ −− }<condition> //Unconditional branches taken JMP ------→
碰到IF條件語句時會預測走不命中分支,碰到循環(while在循環首部除外)時默認進入循環,碰到無條件跳轉則必然走跳轉分支了。
One-level Branch Prediction/Saturating Counter
Saturating Counter可以當作一個狀態機,這類型的Branch Prediction就是記錄該分支的狀態,並根據這個狀態來預測走哪一條分支。
1-bit saturating counter記錄的就是分支上一次的走向,並預測這次的分支會走同一方向。
2-bit saturating counter有如下狀態轉換:
即分支有四種狀態:strongly not taken、weakly not taken、weakly taken、strongly taken。其中not taken的狀態會預測走非跳轉分支,而taken狀態會預測走跳轉分支,並且會根據實際分支為跳轉(T)或者非跳轉(NT)進行狀態的調整。
Two-level adaptive predictor with local history tables
上述的One-level Branch Prediction有個缺陷,以2-bit為例,假設目前某個分鍾的狀態為strongly taken,然后該分支的實際走向為0011-0011-0011(0表示NT,1表示T),但是預測的走向為1100-1100-1100,也就是說該2-bit預測的准確率將為0%。為了改善這個問題,引入了Two-level adaptive predictor with local history tables。
Two-level adaptive predictor如其名字所述,分為兩級:Branch history以及Pattern history table。
Branch history的長度為n bit,用於記錄某個branch上n次的分支走向。
Pattern history table共有2n個項,每個項記錄一個Suturating Counter的狀態。
某個分支在進行分支預測時,會根據該分支上n次歷史分支走向來選擇對應的Pattern history table entry,然后依據其中的狀態來進行預測。在得出實際的分支走向后,也會按照該路線去修改對應table entry中的狀態,然后更新Branch history。
回到上面的例子,如果Branch history的n=2,那么Pattern history table會有四項:00、01、10、11。而0011-0011-0011這種分支選擇方式有規律:00后為1、01后為1、11后為0、10后為0。因此在經過三個周期的分支選擇后,Pattern history table存儲的狀態就能完美預測該分支的下一次走向,也就是說這種n=2的Two-level adaptive predictor就能完美解決上面提出的問題。
不過如果實際的分支走向為0001-0001-0001,那么n=2就顯得不夠了,因為00后可能為0或者1。此時就需要n=3,有000后為1、001后為0、010后為0、100后為0,此時table中的011、101、110、111項為空閑項。
實際上我們可以總結出以下規律:如果某分支的實際走向有固定的周期規律,周期內部有p項,並且該p項內的任意連續n項沒有重復(並且滿足n+1<p<=2n),則n bit的Two-level adaptive predictor就能完美得預測這類型的分支。
Two-level adaptive predictor with global history table
上一小節描述的是local history table,即branch history中存儲的是單個branch的歷史走向,而global history table中存儲的是位於當前branch后方(比當前指令早執行)的n個branches的走向。在local history table實現中,需要為每一個branch維護獨立的Branch history以及Pattern history table,這導致需要大容量的Branch Target Buffer(BTB)來存儲這些數據,不過實際上BTB的容量是有限的。而在global history table中,僅保留一個Branch history以及Pattern history table,能很大程度地節約BTB空間。
不過這種實現的缺點也很明顯,由於只有一個Branch history,也就是說每個分支都是以這個Branch history為基礎來選擇Pattern history table,不過不同的分支也有可能出現相同的Branch history值,這就很難保證一個獨立的分支對應一個獨立Pattern history table entry,也就是說需要較長的Branch history(較大的n,很多現代的微處理器為n=16),以降低不同分支間由於定位到了相同entry帶來的交叉影響。而且也不是每次執行到某個分支時它的Branch history都一樣,Branch history也會改變,這就使得無法定位到所需的entry。另外,由於采用的是不久前執行過的歷史分支來預測當前分支,也就是認為相鄰分支間具有相關性,不過實際上也不一定如此。所以說global history table預測的准確程度是不如local history table的。
Agree Predictor
上面在討論global history table時說到不同分支可能會由於有同一Branch history而定位到同一Pattern history entry,導致不同分支間交叉影響,Agree Predictor為這種情形提供了解決方法。
Agree Predictor采用了global以及local混合的方法。global仍然是采用較長的Branch history以及2-bit Saturating Counter,預測的是某一分支是否與其上次走向相同;local則只用1bit為每個Branch存儲其上一次的分支走向。global的輸出與local的輸出進行異或則能得到分支預測。
上圖中加入了Branch Address用於定位分支,Branch Address也能用於與Branch history經過某種方式的混合(Indexing function)使得定位的Pattern history table entry更准確。
Loop Predictor
一個周期為n的循環在進行分支選擇的時候,會走n-1次跳轉/不跳轉以及1次不跳轉/跳轉,假設n=6,則會形成如111110-111110的形式。因此循環的分支預測算是一種比較容易預測的分支,不過如果循環判斷的次數n非常大,並且體內部有多個分支,那么在Branch history長度有限的情況下,單靠前面所述的global/Agree的預測方式會很難達到較好的預測效果,所以需要一個獨立的Loop Predictor來對循環分支進行預測。
Loop Predictor在第一次對循環分支進行預測時能記錄下該分支的循環周期為n,那么在下一次碰到該循環分支的時候就會預測走n-1次taken/not taken然后走一次not taken/taken。BTB中則需要記錄某個Branch的跳轉目標地址、Branch是否為循環、循環的周期n以及循環在退出的時候是taken還是not taken。
Indirect Branch Predictor
我們前面所討論的分支都是以二叉分支為基礎展開討論,不過分支不總是二叉的。switch以及多態的虛函數在編譯時可能(編譯器相關)會被編譯成 jmp eax/call eax 這類需要通過計算才能得到目標地址的跳轉,這種分支就不是二叉的,而是會有多個候選的目標地址,這就是所謂的Indirect Branch。而前面所述的Branch history是1個bit代表一個分支,這種predictor在碰Indirect Branch的時候只能固定地指定一個跳轉地址,因此是不夠合適的。
在Indirect Branch Predictor中,Branch history用多個bit代表一個分支,如此一來則能很好地適配Indirect Branch。
Prediction of function returns
在函數返回時,會用到 ret/leave 等指令進行跳轉,這些指令是需要從棧中讀取跳回地址后再進行跳轉的,因此也算是一種比較另類Indirect Branch。不過由於這種返回指令總是與 call/enter 成對出現,因此一種較好的處理方法就是在每次進入函數的時候都去讀取其返回地址入棧(這里的棧不是程序的棧,而是Predictor維護的,專門用於返回跳轉),在碰到返回指令時從棧內取出目標地址直接用於Branch Prediction。這就是所謂的return stack buffer機制。
由於這種預測機制用到了stack,也就是說需要 call/enter 跟 ret/leave 成對出現,因此為了保證指令的執行效率,盡量不要用 jmp 來代替函數的跳入跳出指令。
Hybrid Predictor
Hybrid Predictor就是采用多種predictor混合預測,然后從中選擇出對當前branch來說較優的Predictor,以其輸出結果進行分支預測。
Neural branch predictor
采用機器學習來進行分支預測,好處是相比其他predictor預測更為准確,不過相應地需要消耗更多的時間,延遲較大,不過這是早期的說法了。目前AMD Ryzen最新的處理器就是基於神經網絡來進行分支預測。
Reference:
Agner Fog : The microarchitecture of Intel, AMD and VIA CPUs
Intel® 64 and IA-32 Architectures Optimization Reference Manual
Intel 64 and IA-32 Architectures Software Developer's Manual