聊聊流水線處理器

流水線處理模式,相對非流水線,本質上是一種生產管理模式的改變。在硬件條件有空閒的前提下,通過劃分工作步驟,讓硬件處於填滿狀態,從而提升工作效率。在計算機處理器體系結構中,正是採用這種方式來對指令進行處理。本文從流水線的通用原理和流水線冒險兩個角度來聊聊這個話題。

由於編輯器問題導致圖片較小,可訪問個人博客地址閱讀:http://www.lillianyl.com/2017/09/pipeline-processor/

1.流水線的通用原理

1.1 將處理組織成階段

通常一條指令包含很多操作,可以將它們組織成一定的階段序列,從而便於放入一個通用框架來進行流水線處理。

操作 描述
取指(fetch) 從存儲器取指令,再更新PC
譯碼(decode) 從寄存器堆讀出寄存器的值
執行(execute) 運算指令:進行算術邏輯運算;訪存指令:計算存儲器的地址
訪存(memory) load指令:從內存讀出數據;store指令:將數據寫入內存
寫回(write-back) 將數據寫回寄存器堆

將指令拆分成不同階段後,如果每個階段所用的硬件是相互獨立的,那麼可以在對應的硬件電路的每個階段後添加寄存器,從而將單週期處理器改造成了流水線處理器。整個處理過程爲:填充流水線---流水線填滿---流水線排空。在流水線填滿階段,所有的硬件都處於工作狀態,理論上性能爲非流水線的5倍。

但實際上收益並沒有這麼大。每個階段所用的硬件實際並不是相互獨立的;增加的寄存器也會導致延遲增大;每階段的週期劃分也很難做到一致。在後面“流水線的侷限性”和“流水線冒險”部分再來詳細瞭解這個問題,先把注意力放到流水線的原理上來。

 

1.2 計算流水線

這是一個非流水線的硬件系統,對應一條指令的執行,執行其中的組合邏輯部分假設需要300ps (1ps=10^(-12)s),寄存器延遲需要20ps,那麼時鐘週期設置爲320ps(這條指令的執行時間爲一個週期),吞吐量(指令數/指令執行時間)

1/320ps = 3.12GIPS(每秒千兆指令)。

 

 

假設將系統執行的階段劃分爲A,B,C三個階段,每個階段需要100ps,那麼時鐘週期可以設置爲120ps,一條執行執行需要3個時鐘週期,即360ps。在流水線填滿階段,每週期各進入和結束一條執行,系統吞吐量爲 : 1/(120ps) = 8.33GIPS, 提高到了原來的8.33/3.12=2.67倍。

 

1.2 流水線操作的詳細說明

以三階段流水線來說明。在時刻240的時鐘上升沿(在後面的數據冒險中,需要以這個概念的理解爲基礎)來臨之前,I2指令的階段A中的計算結果已經到達第一個流水線寄存器的輸入,但該寄存器的狀態和輸出還保持爲指令I1在A階段中計算的值。I2指令在階段B中計算的值也已經到達第一個流水線寄存器的輸入。當時鐘上升沿來臨時,這些輸入才被加載到流水線寄存器中,引起寄存器輸出的變化。如果時鐘運行的太快,組合邏輯部分還沒計算完畢,這時寄存器的輸入還是非法值,就被寄存器讀取並輸出,就會產生錯誤。

 

1.3 流水線的侷限性

在1.3節前提到的都是理想的流水線系統,每個階段的時間都是相等的。實際上,各個階段的時間是不等的。運行時鐘是由最慢的階段決定的。上圖將時鐘週期設置爲170ps, 在每個時鐘週期,A和C階段都會產生空閒,實際的吞吐量爲 1 / 170ps = 5.88GIPS, 並沒有達到1.1 節中的8.33GIPS.

另外流水線過深,寄存器的增加會造成延遲增大。當延遲增大到時鐘週期的一定比例後,也會成爲流水線吞吐量的一個制約因素。下面是一些主流瀏覽器近幾十年的變更,目前流水線劃分基本穩定在15級左右。階段劃分的越小,週期越短,時鐘頻率越高,即主頻越高,但流水線過深帶來的延遲可能使收益下降,所以並不是主頻越高性能越好。

主流處理器 流水線階段
1993年,Pentium 5級
1995年,Pentium Pro 12級
1999年,ARM9 5級
2002年,ARM11 8級
2004年,Pentuim4(Prescott) 31級
2006年,Core2 Duo(Merom) 14級
2008年,Core i7(Nehalem) 16級
2013年,Core i7(Haswell) 14級

2.流水線冒險

2.1 帶反饋系統的流水線

下述例子來源於《CS:APP》Y86-64指令。

在上述指令序列中,每對相鄰的指令之間都有數據相關。第一條指令將結果存放在%rax;第二條指令要讀取這個值, 並存入%rbx;第三條指令要讀取%rbx。

 

在上述指令序列中,第3條指令產生了一個控制相關。條件測試的結果決定是執行第4行的指令,還是第7行的指令。以上均是流水線引入反饋的例子,硬件結構如下圖。將流水線技術引入處理器時,必須正確處理反饋的影響。

 

2.2 數據冒險

2.2.1 用暫停處理數據冒險

一條指令需要使用之前指令的運算結果,但結果還未被寫回,這種情況稱爲數據冒險。先來看一個例子:

在時鐘週期爲7時,0x017地址的指令執行譯碼的操作,把寄存器%rdx, %rax的值讀入內存。這兩個寄存器的值是由0x000, 0x00a處的指令寫入的,在週期6的時鐘上升沿(1.2中已經闡述過了)到來時,%rdx的值已經發生了變化,爲10;週期7的時鐘上升沿到來時,%rax的值也已經變化。所以在週期7中,0x017的指令執行譯碼時,寄存器的值已經寫入完畢,因此讀取不會出錯。

 

來看一下出錯的情況:

在時鐘週期爲6時,0x017地址的指令執行譯碼的操作,把寄存器%rdx, %rax的值讀入內存。這兩個寄存器的值是由0x000, 0x00a處的指令寫入的。%rdx的值在時鐘週期6的上升沿到來時發生變化,但是%rax的值要在時鐘週期7的上升沿到來時才變化,所以0x017讀取的%rax的值是錯誤的,圖中爲0.

 

處理器可以用暫停的方式來避免這種數據冒險:

通過插入氣泡(bubble),讓指令addq %rdx, %rax停頓在譯碼階段(流水線停頓(stall )),直到產生它的源操作數的指令通過了寫回階段。這個階段並不是什麼都不管,而是必須將機械信號保持爲狀態改變前的值。實現這樣的機制並不困難,可以用軟件的解決方案,即在指令之中插入nop指令,但是我們一般希望對軟件屏蔽細節,所以能過硬件來完成更好。但是流水線暫停會帶來吞吐量下降的問題,所以這並不是最佳的方案。

 

2.2.2 用轉發處理數據冒險(也叫數據前遞(forwadding), 旁路(bypass))

轉發的思想是,與其暫停流水線直到寫完成,不如簡單將要寫的值(還在內存中,尚未寫入寄存器)傳到所需要的地方作爲源操作數 。在圖中標註的1處:0x00a地址的指令在週期6將%rax和3的值讀入內存,賦值給內存的臨時變量W_dstE和W_valE;在圖中標註的2處,當時鍾週期7的上升沿到來時,3的值纔會被寫入寄存器,但是我們不用等待這個時刻的到來;在圖中標註的3處,通過直接把內存中的值W_valE賦值給valB, 0x016地址指令的譯碼操作可以正確執行。

這種方式在硬件的實現上,由於繞過了寄存器,所以又稱爲旁路(bypass)。

 

2.2.3 將暫停和轉發結合來處理數據冒險(加載/使用冒險)

2.2.2中用轉發的方式,來將還在內存中的值,直接傳遞給下一條指令作爲源操作數。但是如果內存讀在流水線發生得比較晚,單純的轉發就解決不了。如下例子:

0x032地址的指令在譯碼階段需要用到%rbx和%rax作爲源操作數,這兩個寄存器要分別在時鐘週期8的上升沿和時鐘週期9的上升沿才寫入。對於%rbx,可以使用轉發的方式提前從內存獲取;但是%rax的值,要等到時鐘週期8才寫入內存,太晚了。這種情況的數據冒險,發生在使用加載指令(mrmovq和popq時),因此又稱爲加載/使用冒險,只能採用將流水線暫停一個週期,再轉發的方式來解決。

 

暫停了一個時鐘週期後,可以直接從內存獲取%rax和%rbx的值了。

 

2.2.4 避免ret指令造成的控制冒險

對於ret指令,參考以下程序(來源於《CS:APP》)

在上述程序中,指令列出的順序與它們在程序中執行的程序並不相同。當ret執行執行後,ret執行的下一條指令並不執行,而是返回調用者的地址。因此,需要在流水線中插入3個bubble,讓ret指令結束譯碼、執行、訪存階段(獲取到了%rip應該設置的值)後,再正常進行流水線的操作,如下圖。

 

2.2.5 避免跳轉指令造成的控制冒險

轉移指令和流水線本質上是衝突的,因爲轉移指令是要改變流向,而流水線希望指令依次取出。在處理轉移指令時,處理器採用了分支猜測的技術,即猜測分支方向並根據猜測來取指。預測策略有總是選擇、從不選擇、反向選擇、正向不選擇等。當猜測錯誤時,會產生轉移開銷,包括:

1.將錯誤執行的指令廢除(即“排空流水線”)

2.從轉移目標地址重新取指

參考以下程序:

下圖展示瞭如何處理預測錯誤的分支:

在週期4發現預測錯誤之前,已經取出了兩條指令。在週期5中,流水線往這兩條指令中插入bubble(插入氣泡是保持機械信號狀態不變),在週期6取出跳轉指令後面的那條執行。這樣這兩條錯誤指令就從流水線上消失了。

 

2.2.6 結構冒險

結構冒險指的是對同一個寄存器的讀和寫同時發生的情況,其在處理器設計之初就已經考慮並在硬件上解決了:指令的單個階段的執行其實用不了一個週期,在讀和寫同時發生時,前半週期用於寫,後半週期用於讀。

 

3.性能分析

所有插入的氣泡,都會導致流水線週期的損失,設執行了Ci條有效指令,Cb條氣泡,則共(Ci+Cb)個時鐘週期 , 用每指令週期數(CPI, cycles per instruction)來衡量性能(理想情況是不產氣泡,即CPI爲1):

CPI = (Ci+Cb)/Ci = 1 + (Cb/Ci)

假設條件轉移指令佔所有指令的20%, 其中40%的機率預測錯誤,預測錯誤會產生2個氣泡;

假設ret指令佔所有指令的2%, 每次產生3個氣泡;

假設需要加載指令佔所有指令的25%, 其中20%會導致加載/使用冒險,每次產生1個氣泡,那麼CPI:

CPI =  1 + 20%*40%*2 + 2%*3 + 25%*20%*1 = 1.27

4.參考

1.《深入理解計算機體系統》第三版

2. coursera 北京大學《計算機組成》

3. CMU《cs:app》課程官網

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章