1. 程式人生 > 其它 >CSAPP(四)下——流水線CPU——SEQ+實現 處理器體系結構

CSAPP(四)下——流水線CPU——SEQ+實現 處理器體系結構

流水線通用原理

在之前的SEQ模型中,由於一條指令的所有部分必須在一個時鐘週期內完成,所以時鐘週期無比的慢,所以系統的吞吐量就很慢,吞吐量即每秒能夠處理的指令數量。

流水線系統的思路和工廠流水線一樣,比如一個服裝工廠流水線需要通過裁剪、縫合、貼商標、裝袋這四個步驟,想象一下是當一件衣服的這四個步驟全部完成之後再開啟下一件衣服的步驟快,還是第一件衣服的裁剪過程完畢後到達縫合階段,就讓下一件衣服進入裁剪階段快。肯定是第二種,這就是流水線思想。

非流水線時,一個指令要在一個時鐘週期內完成,流水線化之後,每一個階段都是一個時鐘週期

同時也要注意,流水線的前提是每一個階段之間都是相互獨立的,它們不會相互影響,即一個處於裁剪階段的衣服不會影響到另一個處於縫合階段的衣服。我們下面為了講解流水線的工作過程會把指令拆開成幾個階段,並假設它們都是獨立的。當我們瞭解了流水線的工作流程後,我們會把目光移向現實,即拆解後的指令中的各個部分並非獨立的。

計算流水線

我們還是要把目光放到計算上,一個指令的執行可以看作是一個輸入進入到一個大的組合邏輯電路中,組合邏輯電路經過一段時間的延時產生結果,結果儲存到暫存器中。單位ps是皮秒,也就是\(10^{-12}\)秒。組合邏輯電路會產生一些延時(這裡是300ps),結果暫存器的裝載又需要一些延遲(這裡是20ps)。

有了量化指令執行時間的方法後,下面給出系統吞吐量的公式:

\[吞吐量T=\frac{1條指令}{指令進入系統的時間間隔(其實也就是時鐘週期)}\times \frac{1000ps}{1ns}\\ \]

上圖中的未流水線化的系統的吞吐量是

\[T=\frac{1}{(300+20)ps}\times \frac{1000ps}{1ns} \approx 3.12GIPS \]

單位GIPS是每秒鐘執行多少千兆個指令,也就是每秒執行幾十億個指令。

除了吞吐量外,另一個量化指標是延遲,延遲是一條指令完整執行所需要的時間,這裡就是\(300+20=320\)

一個流水線系統如下所示,指令被分割成若干個階段,下圖中是3個,每個階段都有一個儲存結果的暫存器,它用於儲存此階段的狀態,然後下階段時讀取。因為下個階段已經是另一個時鐘週期了,所以下個階段能讀取到上個階段寫入的值。稍後我們會看到暫存器是如何被流水線中的指令寫入的。

上面使用流水線後,系統的吞吐量得到了很大提升,因為指令進入系統的間隔小了(\(100ps+20ps\)),也可以理解為指令進入系統的頻率快了。

\[吞吐量T=\frac{1}{(100+20)ps} \times \frac{1000ps}{1ns} = 8.33GIPS\\ ...\\ 延遲=(100 + 20) \times 3 = 360ps \]

這裡我們也注意到,將指令拆分為多個階段以帶來吞吐量提升的副作用是,由於每個階段附加的結果暫存器,延時也會越來越大

。並且吞吐量的提升有個瓶頸,在稍後的習題中我們會體會到這一點,達到瓶頸後再拆分,吞吐量不會上升,延時會越來越大。

結果暫存器的值推進

當你的流水線像上圖一樣被拆分成三段時,你的系統中同時執行的指令最多就只會有三個。每一個時鐘週期,都會有一個系統中最老的指令執行完畢,退出流水線,也會有一個新的指令進入流水線。下面我們看看這三條指令如何共用三個結果暫存器而不會產生錯亂。

在時間刻度120之前,I1的A階段的輸出已經寫入到第一個暫存器的輸入端,但直到120後時鐘上升沿到來時,這個輸出才會到達第一個暫存器的輸出端,被I1的B階段讀取。當然,I2的A階段也能讀取到這個暫存器的值,關鍵的是它不會去讀取,因為它是指令的第一個階段。

而當I2需要讀取第一個暫存器時,那就是I2的B階段了,此時I1已經不需要第一個暫存器了,它在C階段,它需要的是第二個暫存器。

所以,每條指令的每個階段讀取它上一個階段(如果有的話)儲存的暫存器值,並且將結果輸出到本階段的暫存器輸入端,等待下一個時鐘上升沿把該輸出推到暫存器的輸出端,然後被本條指令的下一個階段(如果有的話)讀取。就是這樣迴圈往復的過程。

流水線的侷限性

不一致的劃分

雖然但是...我們很難將流水線中的每個階段的時間長度劃分成一樣的。而且,流水線的特性就是:時鐘週期的時間受最長的階段影響。因為時鐘週期如果比某個階段所需的時間長一些,那沒啥問題,因為時鐘會處理好它的輸出結果讓下一個階段能讀到,但如果時鐘週期比某個階段所需要的時間短,那它的操作還沒完成,下一個階段就開始了,並且會從暫存器中讀到錯誤的值,然後世界就毀滅了。

下圖是一個例子

習題4.28

流水線過深,收益反而下降

吞吐量的計算總是能帶給我們一種假象,就是當我們無限的細分指令的階段時,系統性能就會得到無限的提升。吞吐量提升的同時,每個階段需要的結果暫存器的延時也會變大,所以需要權衡。

現代處理器通常採用很深(15或更多)的流水線。處理器架構師將指令劃分成很小的階段,保證每階段的延時更小,電路設計者也會盡力的設計暫存器,讓暫存器的延遲更小。

習題4.29

\[latency(k) = (\frac{300}{k} + 20) \times k = 300 + 20k\\ T(k) = \frac{1}{\frac{300}{k} + 20} \times \frac{1000ps}{10ns} \] \[lim_{k->\infty}T(k)=50GIPS \]

要注意的是,當k趨近於無窮時,由於暫存器延遲不會改變,所以吞吐量是有上界的,但是指令執行延遲則是會趨近於無窮。

帶反饋的流水線系統

上面我們的所有內容都基於一個假設:指令之間是獨立的

獨立,代表它們不會共用暫存器檔案中某個相同的暫存器,某一條指令是否執行不會與其它指令相關

一旦兩條指令不獨立,那麼將它們放到相同的流水線中就非常危險:

想象1和2行兩條指令在流水線中共同執行,它們同時操作%rax%,它們可能得到由對方的某個階段設定的結果,而非自己需要的結果。

想象第2條,3條和第4條指令,第二條的計算結果會影響到第三條的判斷,第三條的判斷會影響到是該執行第7條還是第4條。當一條的結果會給另一條帶來副作用時,它們怎麼能一起執行?

在SEQ中,修改的暫存器檔案的值和新的PC計數器的值都會通過下圖右面的被稱作反饋線路的東西寫回,寫回在整個指令處理的最後階段。

在SEQ中,這個反饋線路寫入的東西都會被傳遞給下一條指令,因為指令件是無交叉的順序執行。但如果你把這種反饋線路設計引入到階段數為k的流水線系統中,反饋將不會傳遞給下一條指令,而是會被傳遞後面第k條指令。

Y86-64流水線實現

SEQ+ —— 重新安排計算階段

SEQ+在SEQ的基礎上做了一寫改動,我們把PC的計算挪到最開始的階段,而非最後的階段。除此之外再沒有其它改動,它只是我們稍後將處理器流水線化的一個前置工作,目前還解決不了上面所述的那些指令間的副作用問題。

WHAT??PC計算怎麼能挪到最開始的階段?就像下面的指令jXX,更新PC的階段實際上依賴了前面階段的狀態訊號,ifunCndvalCvalP,在一些其它操作上,還有可能依賴valMicode等。所以怎麼可能在這些狀態還沒得到的情況下更新PC?

SEQ+使用的辦法是,維護一些狀態暫存器pIcodepCndpValM等來儲存上面那些狀態,這些暫存器儲存的是前一個時鐘週期裡產生的狀態訊號,而當一個新的時鐘週期開始時,處理器會根據這些狀態暫存器中的值來計算該執行指令的PC

讀者請注意,現在只是在運算階段排序上做出了一個調整,實際上稍後就會看到,這第一個階段計算出來的PC只是一個預測的PC。畢竟按照這種模型,當前指令的PC確定時,它的上一條指令才執行到確定PC的後一個階段而已。

可以看到,SEQ+中沒有實際的PC暫存器,每條指令的PC都是動態計算的。所以我們可以以與概念模型不同甚至大相徑庭的任意方式來實現處理器中的任何細節來提高效能,只要最終效果與概念模型中宣稱的效果一樣即可。在你產品上工作的人只關心產品是否正確,是否高效,不關心底層的實現細節。這也是公有設計,私有實現的一個體現。

插入流水線暫存器

下面我們將SEQ+流水線化,這需要我們在每個階段中插入流水線暫存器。

  1. F:儲存程式計數器的預測值
  2. D:位於取指和譯碼階段期間,儲存最新取出的指令資訊,即將由譯碼階段進行處理
  3. E:位於譯碼和執行階段期間,儲存最新的譯碼指令和從暫存器讀出的值的資訊,即將由執行階段進行處理
  4. M:位於執行和訪存階段期間,儲存最新的指令執行結果,即將由訪存階段進行處理。還將儲存用於處理條件轉移的分支條件和分支目標資訊。
  5. W:位於訪存階段和反饋路徑之間,反饋路徑將計算出來的值提供給暫存器檔案寫,而當完成ret指令時,它還要向PC選擇邏輯提供返回地址。

通過向SEQ+中新增流水線暫存器,我們建立了一個新的CPU,命名為PIPE-,名字中的-代表與最終的CPU相比,它的效能還是要差一些。下圖是PIPE-CPU的硬體結構。

上面的硬體結構十分複雜,對於我這種對硬體理解十分淺薄的很難理解,不過我們並不需要完全理解這張圖。所以對該圖的解釋,本篇文章裡就沒有了,該圖中的連線與Y86-64指令集中的各個階段之間的關聯是一致的,原書中對次進行了一些介紹。

對訊號進行重新排列和標號

從上圖的結構中,可以看到順序實現中的valCsrcAstat這種訊號值在每個流水線暫存器中都有儲存,這些版本會隨著指令一起流過系統。

為了清晰,我們將這些值與相應的流水線暫存器的名字聯絡起來,比如D_stat,代表流水線暫存器D中的stat訊號,也就是取指後譯碼前的stat訊號。類似的還有E_statM_stat。同時我們還需要為在某個階段剛剛計算出來的還沒存到流水線暫存器中的訊號命名,它們的命名方式為小寫的階段名加訊號名,比如在執行階段的state_stat,訪存階段的為m_stat

整個處理器的實際狀態Stat是根據流水線暫存器W中的值通過寫回階段中的塊計算出來的。

預測下一個PC

流水線模型讓我們在每一個時鐘週期裡都必須載入一個新的指令,但由於條件分支命令jXX的下一條指令究竟執行啥需要等到幾個時鐘週期之後,jXX已經完成了執行階段才能確定,對於ret,則是需要等到訪存階段才能確定。對於其它指令,包括call和無條件jmp,我們都能在取指階段完成後就確定下一條指令的地址,所以,通過預測在一開始就確定下一條指令的PC,在大部分情況下預測是可靠的,能達到每個時鐘週期載入一條指令的效果。對於條件轉移來說,我們可以預測條件滿足或不滿足,但預測總會出錯,出錯時就要有相應的補救機制。在PIPE-的實現中總是選擇假設條件滿足。

在PIPE-的實現中,ret指令沒有做任何預測,只是簡單的暫停接收新指令,直到ret通過寫回階段。但由於呼叫和返回總是成對出現,所以高效能的CPU設計會通過一個棧來預測ret的返回值,這個預測通常要比分支預測可靠。

流水線冒險

PIPE-目前還沒解決兩個非獨立的指令會互相影響的問題,但已經為此做了足夠的努力,現在我們可以直接在PIPE-上構建一些邏輯來解決這些問題。

非獨立指令之間的影響主要是資料相關控制相關的,處理器在執行非獨立指令時就像在冒險,因為有可能出現錯誤的情況,所以就有資料冒險控制冒險

下面是一個例子,兩條irmovq指令分別儲存%rdx%rax,後面的addq對這兩個暫存器中的值相加。前兩條指令和後面的addq不是獨立的,因為前兩條的結果會影響到addq

在週期6時,第二個irmovq指令正在執行了寫回階段,addq還沒有開始執行,到週期7時,第二個irmovq已經將它的值寫回,所以addq會讀到兩個正確的值。addq能正確處理的原因是中間被插入了三條nop指令,它們不會修改處理器的任何狀態,只是為了給addq的執行帶來一些延遲。

下圖在上一個的基礎上拿掉了一個nop,我不理解的是,指令的F階段不是預測PC並取指嗎,不會做譯碼操作啊,所以我感覺兩個nop也沒啥問題啊。順著書來吧,如果有能解釋的請幫幫孩子。

按(書上的)這個來說,一條指令的運算元只要被它前面三條指令修改過就會出現資料冒險。

可能發生冒險的地方有:

  1. 程式暫存器:如上面所示
  2. 程式計數器:這個是控制冒險的範疇,如ret指令
  3. 記憶體:我們的處理器沒有這種情況,因為我們的設計中對記憶體的讀寫都在訪存階段,後一個指令到達訪存階段時,前一個指令必然已經完成訪存階段了。
  4. 條件碼暫存器:我們的處理器也沒有這種情況,寫這些暫存器的操作都在執行階段寫入,讀這些暫存器的操作都在執行或訪存階段讀,和記憶體一樣,這裡不存在冒險。
  5. 狀態暫存器:

用暫停來處理資料冒險

即動態插入名為bubble的,和nop功能一樣的指令,目的是為了延遲後面可能會出現冒險的指令的執行。

用轉發來避免資料冒險

你必須在一條指令的譯碼階段暫停執行的原因是:上一條指令計算出的結果必須等到寫回階段才能寫入到暫存器中。實際上,最早在執行階段它可能就已經計算完了結果並儲存到M_valE中。同樣,在訪存階段和寫回階段都存在這種可行的轉發,e_valEm_valMM_valEW_valMW_valE都可以作為轉發埠。所以,轉發技術就是將本條指令在譯碼階段要讀取的暫存器值直接轉發到上一條指令的某個結果上。從硬體上看,這只是相當於一次流水線暫存器的訪問。

下圖就是第二條指令對%rax的寫入(本來寫入到流水線暫存器)直接被addq指令所讀取。

下面是加上五條轉發線和對應硬體的圖。

載入/使用資料冒險

並非所有資料冒險都能用轉發技術處理。轉發技術只能處理後一條指令需要的資料前一條指令已經計算出來由於尚未走完寫回階段而沒寫到暫存器檔案中的情況。

考慮下一條指令需要上一條指令的訪存階段拿出的值作為源運算元時,就沒法使用轉發技術。因為訪存發生在流水線很靠後的部分,下一條指令的譯碼階段就算來了,上一條的訪存也沒執行呢。

這時可以通過結合暫停和轉發來避免這種資料冒險

避免控制冒險

對於ret指令,我們的處理器選擇的方法就是簡單的暫停,前面也說過了。

對於條件分支,我們的處理器會選擇條件滿足的分支來執行,而當邏輯分支在執行階段發現我們選擇的條件分支是錯的時,錯誤的分支已經有兩條指令被載入並執行了。好在它們還沒有產生任何作用,因為它們還沒到執行階段,前面的所有操作都是隻讀的。CPU只需要簡單的扔掉它們就行。只是有兩個時鐘週期浪費掉了。