流水线问题--计算机体系结构
參考書:《計算機體系結構量化研究方法》 作者:John L. Hennessy
一、 基本概念
先理解幾個基本概念:
流水線:是一種將多條指令重疊執行的實現技術。一條指令的執行需要多個操作,流水線技術充分利用了這些操作之間的并行性。
流水級:不同步驟并行完成不同指令的不同部分。這些步驟中的每一步都稱為流水級或流水段
吞吐量:由指令退出流水線的頻率決定。
處理器周期:一條指令在流水線中下移一步所需要的時間。一個處理器周期通常為1個時鐘周期。(又是為2個,但要少見的多)
流水線的作用:縮短每條指令的平均執行時間。降低了每條指令時鐘周期數(CPI)
1.1 RISC精簡指令集基礎知識
以MIPS指令集為例,其提供了32個寄存器,通常由以下三類指令:
1)ALU指令:取兩個寄存器或者一個寄存器與一個符號擴展立即數進行運算并存儲到第三個寄存器中
2)載入和存儲指令:這些指令獲取一個寄存器源(基址寄存器)和一個立即數字段(16位,偏移量),作為操作數計算有效地址用作存儲器地址。
3)分支與跳轉:條件轉移。通常由兩種方式指定分支條件:其一,采用一組條件位(條件碼);其二通過兩個寄存器之間、寄存器與0之間的對比來設定。MIPS采用后者。
1.2 RISC指令集的簡單實現
RISC子集中的每條指令都可以在最多5個時鐘周期內實現,如下
1)指令提取周期(IF)
將程序計數器PC發送存儲器,從存儲器提取當前指令。向程序計數器加4(因為每條指令的長度為4個字節),將程序計數器更新到下一個連續計數器。
此周期操作:a.讀取PC,b.更新PC
2)指令譯碼/寄存器提取周期(ID)
此周期操作:a.將指令譯碼,b.讀取相應寄存器,c.判斷是否為分支
這幾點在下面都會有詳細解釋
3)執行/有效地址周期(EX)
此周期操作(其中之一):a.計算有效地址(基址寄存器+偏移量)b.在寄存器與寄存器之間進行運算,c.在寄存器與立即數之間進行運算
4)存儲器訪問(MEM)
操作(執行其中之一):a.將寄存器寫入存儲器(寫入)b.將存儲器的內容讀取到寄存器中(載入)
5)寫回周期(WB)
將程序結果(來自ALU運算或載入指令)寫入寄存器堆
1.3 RISC經典五級流水線
下面這種情況沒有采用流水線結構,是簡單的順序執行,在執行完一個指令的五個周期后,才能執行下一條指令,如圖所示:
這種方法存在著一個問題,比如參與運算的ALU單元平均在五個周期種只被使用了一次,硬件資源造成的很大程度上的浪費。于是我們采用了流水線結構,如下圖所示:
流水線結構在每個時鐘周期都會啟動一條指令,其性能會達到非流水化處理器的5倍(理想情況下)。比如ALU單元,在每一個時鐘周期都被用到了,相比于非流水化大大提高了硬件的利用率。IF ID EX MEM WB值得是指令集的5個時鐘周期。
可能很難相信流水線是這么地簡單,實際上,它的確不是這么簡單。
下圖是流水化形式的RISC數據路徑的簡化版本。如圖:
流水線可以看作一系列隨時間移動的數據路徑。上圖給出了數據路徑不同部分之間的重疊,時鐘周期數5(CC5)表示穩定狀態,此狀態下,一個時鐘周期內,運行著五條指令的不同部分。寄存器左側虛線表示寫入,右側虛線表示讀取。IM表示指令存儲器,DM表示數據存儲器,CC表示時鐘周期。
我們也可以看出來,主要功能單元(如ALU)是在不同周期使用的,因此多條指令的重疊執行不會引發多少沖突(實際上,不可避免會存在些許沖突即冒險問題,在后面會提到及其解決方案)。以下三點可以看出:
1)采用分離的指令存儲器及數據存儲器,并采用分離緩存的方式避免二者的訪問沖突。不過相應開銷是:存儲器系統必須提供5倍的帶寬。
2)在兩個階段都使用了寄存器堆(MIPS中由32個寄存器):一是在ID中進行讀取,一是在WB中進行寫入。每次時鐘周期會執行兩次讀取和一次寫入。我們在時鐘的前半周期寫寄存器,后半周期讀寄存器
3)為了每個時鐘周期都會啟動一條新指令,必須在IF階段增加一加法器,一邊為下一條指令做準備。此外,我們會在ID期間計算潛在的分支目標。
你會發現流水線最重要的就是:
1. 確保流水線中的指令不會試圖在相同時間使用硬件資源。
2. 不同流水級中的指令不會互相干擾
因此在連續流水級中引入了流水線寄存器,如圖所示
在流水化處理器中,如果要將中間結果從一級傳送到另外一級,而源寄存器與目標地址可能并非直接相鄰,這時候就是流水線寄存器發揮作用的時候了。例如:要在存儲指令中存儲的寄存器值是在ID期間進行讀取的,但要等到MEM才會真正用到;他在MEM級中通過兩個流水線寄存器傳送到數據寄存器。于此類似,ALU指令的結果是在EX期間計算的,但要等到WB才會實際存儲;
所以有必要對每個寄存器進行命名,稱為:IF/ID,ID/EX,EX/MEM,MEM/WB。
流水化作用:提高了CPU指令吞吐量(單位時間內完成的指令數)。但不會縮短單條指令的執行時間。實際上它還會產生額外的開銷
流水線開銷:流水線寄存器延遲和時鐘偏差。
二、 流水化冒險
上面介紹的流水線,我們假定指令之間都沒有相互依賴關系。可實際上,流水線中的指令可能是存在依賴關系的。
若產生了依賴關系,再按照之前無停頓的順序執行,就會產生冒險問題。有以下三種冒險:
1)結構冒險:比如兩個指令都要訪問存儲器端口,可存儲器只有一個寫端口,這咋辦?這時候就產生了資源沖突即結構冒險。
2)數據冒險:實際上,指令是存在一些先后順序的,如果一些指令取決于先前的指令的結果,就可能導致數據冒險。
3)控制冒險:比如C語言中的if else語句,分支指令及其他改變程序計數器的指令實現流水化時可能會導致控制冒險。
這三種冒險總結如下表所示:
2.1 結構冒險
出現原因:
如CC4階段,同時對存儲器進行了引用,如果只有一個存儲器端口,就會產生沖突。
當指令序列遇到這種冒險時,流水線將會使這些指令中的一個停頓,直到所需單元可用為止。這種停頓會增大CPI的值,使其不再是理想的1。我們把這些停頓稱為流水線氣泡或氣泡。解決流圖如下:
載入指令強占了指令提取周期,導致流水線停頓。雖然浪費了流水線中的一個時鐘周期,但避免了沖突。
2.2 數據冒險
舉個栗子,以下指令:
DADD R1,R2,R3 ;R1=R2+R3 DSUB R4,R1,R5 ;R4=R1-R5 AND R6,R1,R7 OR R8,R1,R9 XOR R10,R1,R11DADD之后的指令都用到了DADD指令的結果。如圖所示:
DADD在WB流水級中寫入R1的值,但DSUB在其ID中就要讀取這個值,這個問題稱為數據冒險。因為后面三條指令中使用DADD指令的結果時,由于要等到這些指令讀取寄存器之后才會向其中寫入,所以會導致冒險。
2.2.1 轉發技術
這一問題,可以采用轉發(forwarding)的簡單硬件來解決(也稱為旁路或短路)。此技術關鍵是:認識到DSUB要等到DADD實際生成結果之后才會真正用到它。DADD將此結果放在流水線寄存器中,然后轉發,轉發工作方式如下:
1)來自EX/MEM和MEM/WB流水線寄存器的ALU結果總是被反饋回ALU的輸入端
2)如果轉發硬件檢測到前一個ALU操作對當前ALU操作的源寄存器進行了寫入操作,則控制邏輯選擇轉發結果作為ALU的輸入,而不是從寄存器堆中讀取。
如圖所示:
這種情況不需要停頓,但是并非所有的數據冒險都可以通過轉發解決。這就是下面提到的需要停頓的數據冒險
2.2.2 需要停頓的數據冒險
LD R1,0(R2) DSUB R4,R1,R5 AND R6,R1,R7 OR R8,R1,R9這個例子與上面的不同在于,上面的指令是ALU運算指令,其在EX階段就能產生結果,但是載入指令LD就不能在當前周期產生結果,其必須在MEM周期得到結果。因此就沒辦法僅通過轉發技術就消除這一冒險。我們需要增加一種稱為流水線互鎖以保持正確的執行模式。
流水線互鎖會導致流水線停頓,讓需要使用某一數據的指令等待,直到源指令生成該數據為止。這種流水線互鎖會引入一次停頓或者氣泡。如圖:
2.3 分支冒險(控制冒險)
對于MIPS流水線,控制冒險造成的性能損失可能比數據冒險還要大。在執行分支時,修改后的程序計數器的值可能等于也可能不等于當前值加4。介紹一個概念:
選中分支:如果分支將程序計數器改為其目標地址,就代表選中分支,否則就是未選中位置。比如指令i是選中分支,通常會等到ID末尾,完成地址計算和對比之后才會改變程序計數器。
處理分支最簡單的方法是:在ID期間(此時對指令進行譯碼)檢測到分支,就對分支之后的指令重新取值。第一個IF周期基本上就是一次停頓,因為它從來不會執行有用工作。如圖所示:
如果每個分支產生一個停頓,竟會使性能損失10%-20%,具體取決于分支頻率,所以要研究一些用于應對這一損失的技術。
有多種方法可以處理由分支延遲導致的流水線停頓,我們討論四種簡單的編譯機制。我也很好奇它為啥會有這么多解決方案???
2.3.1 凍結或沖刷流水線
保留或刪除分支之后的所有命令,直到知道目標分支為止。這也是上圖的解決方案。這種情況下,開銷是固定的,不能通過軟件來縮減。
2.3.2 預測未選中機制
繼續提取指令。將每一條分支都看作未選中分支,允許硬件繼續執行,就好像分支未被執行一樣。如果分支被選中,就需要將已經提取的指令轉為空操作,重新開始在目標地址提取指令。但這時候必須特別小心,因為在確切知道分支輸入之前,不要改變處理器狀態。復雜性在于:必須知道處理器狀態可能何時被指令改變,以及如何撤銷這種改變。實現過程如圖所示:
2.3.3 預測選中機制
將所有分支都看作選中分支。只要對分支指令進行了譯碼并計算了目標地址,我們就假定該分支被選中,開始在目標位置提取和執行。
2.3.4 延遲分支
這種技術在RISC處理器種使用的非常廣泛。在延遲分支種,帶有一個分支延遲的執行周期為:
分支指令
依序后續指令
選中時的分支目標
依序后續指令位于分支延遲的間隔中。無論分支是否被選中,這一指令都會執行。我們要做的任務就是讓這些必須進行的指令有效且可用。首先看一下延遲分支的行為特性。
盡管分支延遲可能長于1個時鐘周期,但在實際中,幾乎所有具有延遲的分支的處理器都會只有單個指令延遲。為了讓這些指令延遲有效并可用,使用了多種優化方式,如下:
延遲分支的局限性在于
a. 對于可排在延遲時隙中的指令有限制
b. 在編譯時預測分支是否可能被選中的能力有限。
這種優化也都引入了一種取消或廢除分支。在取消分支中,指令包含了預測分支的方向。當分支的行為與預期一致時,分支延遲時隙中的指令就像普通的延遲分支一樣執行。不過也有可能把分支預測錯誤,此時,分支延遲時隙中的指令轉為空操作。
這四種方法的不同是:
凍結或沖刷流水線導致對分支后續指令重新取值。此時會有一周期停頓;
預測選中或未選中機制,導致選中分支目標后續指令變為空操作。
延遲分支機制繼續執行,不過采用調度,使其執行的指令盡可能有用。如果調度錯誤會將該分支轉為空操作。
三、流水化的實現
3.1 MIPS的簡單實現
首先給出一種簡單的非流水化實現,然后介紹流水化實現
每種MIPS指令都可以在最多5個時鐘周期中實現,分述如下:
1)指令提取周期(IF)
Mem[PC] ==> IR
PC + 4 ==> NPC
操作:送出PC到IR中,將PC遞增4。IR保存將在后續時鐘周期中需要的指令。NPC用于保存下一順序PC
2)指令譯碼/寄存器提取周期(ID)
Reg[rs] == > A
Reg[rt] == > B
IR的符號擴展立即數字段==> Imm
操作:對該指令進行譯碼,并訪問寄存器堆(rs,rt為源寄存器)放到臨時寄存器A和B中,IR的低16位也進行了符號擴展,并存儲到臨時寄存器Imm中,供下一個周期使用。
3)執行/有效地址周期(EX)
ALU對前一周期準備的操作數進行操作,根據MIPS指令類型執行以下4中功能之一
存儲器引用
A + Imm ==> ALUOUTPUT
操作:ALU將操作數相加,得到實際地址,并將結果放在ALUOUTPUT中
寄存器-寄存器ALU指令
A func B ==> ALUOUTPUT
操作:對A,B中的取值執行由功能代碼指定的操作,將結果放在ALUOUTPUT中
寄存器-立即數ALU指令
A op Imm ==> ALUOUTPUT
操作:對A,Imm中的取值執行由操作代碼指定的操作,將結果放在ALUOUTPUT中
分支
NPC + (Imm << 2)==> ALUOUTPUT
(A == 0) ==> Cond
操作:ALU將NPC加到Imm中的符號擴展立即數,將立即數左移2位,得到一個字偏移量,以計算分支目標。Cond為0是分支的標志。
4)存儲器訪問(MEM)
對所有的指令更新PC:NPC ==> PC
存儲器引用
Mem[ALUOUTPUT] ==> LMD 或
B ==> Mem[ALUOUTPUT]
操作:訪問寄存器。如果指令為載入指令,則從存儲器返回數據,將其放入LMD(載入存儲器數據)寄存器中。其都需要在ALUOUT存放的ALU計算得到的實際地址。
分支
If (cond) ALUOUTPUT ==> PC
操作:如果指令為分支指令,則用寄存器ALUOUTPUT中的分支目標地址代替PC
5)寫回周期(WB)
寄存器-寄存器ALU指令
ALUOUTPUT ==> Regs[rd]
寄存器-立即數ALU指令
ALUOUTPUT ==> Regs[rt]
載入指令
LMD ==> Regs[rt]
操作:無論結果來自存儲器系統(在LMD中)還是ALU,都將其寫到寄存器堆中。
此中,rs,rt為兩個源寄存器,rd為目的寄存器。
同時出現一個問題,在寫回周期的載入指令中,為什么非要通過LMD才能寫回寄存器呢?看下面這個數據流圖就明白了:
因為存儲器數據只能容國LMD存儲至寄存器堆
此中,mux為多工器即數據選擇器。
3.2 MIPS基本流水線
上圖為順序結構,我們幾乎不需要什么改變就可以對上圖的數據路徑實現流水線。如圖:
流水線寄存器用于從一個流水級向下一個流水級傳送數據和控制。每個流水級上的事件如下:
注意:前兩級的操作與當前指令類型無關。由于要等到ID級結束時才會對指令進行譯碼,所以前兩級操作必須與當前指令無關。IF行為取決于EX/MEM中的指令是否為選中分支。如果是,則會在IF結束時將EX/MEM中的分支指令的目標地址寫入PC中,如果不是,則寫回遞增后的PC.
分析一下多工器的作用:
ALU級的兩個多工器根據指令類型設定,由ID/EX寄存器的IR字段規定。上面的ALU輸入多工器根據該指令是否為分支來設定,下面的多工器根據指令時寄存器-寄存器操作,還是ALU操作還是任意其他類型的操作來設定的。IF級的多工器選擇是遞增PC的值,還是EX/MEM.ALUOUTPUT的值來寫入PC。這個多工器由EX/MEM.Cond字段控制。第四個多工器由WB級的指令是載入指令還是ALU指令來控制。
3.3 處理分支問題
在MIPS中,分支需要對比兩個寄存器的值,在ID周期結束時完成此判斷。為了充分利用盡早判斷出該分支是否命中懂得優勢,都必須盡早計算PC。在ID期間計算分支目標地址需要一個加法器。下圖為修改后額流水化數據路徑。
增加獨立的加法器在ID期間做出分支判斷,分支僅需要停頓1個時鐘周期
四、交叉問題/記分卡
如果存在不可避免地冒險,冒險檢測硬件會使流水線停頓。在清除這種相關性之前,不會提取或發射指令。為了彌補這些性能地損失,編譯器嘗試調度指令來避免冒險;這種方法稱為編譯器調度或靜態調度。
目前為止,討論的所有技術都是使用循序指令發射,無調度,這意味著如果一條指令在流水線中停頓,將不能處理后續指令。在采用循序發射時,如果兩條指令之間存在冒險,即使后面存在一些不相關的、不會停頓的指令,流水線也會停頓。
在流水線中,結構性冒險和數據冒險都是在指令譯碼ID期間進行檢查的:當一條指令可以正確執行時,也是從ID發射出去的。我們必須將發射過程分為兩部分:檢查結構性冒險以及等待數據冒險的消失。循序對指令進行譯碼和發射;但是,我們希望指令在其數據操作數可用是立即開始執行。因此流水線是亂序執行的,也就暗示是亂序完成的。為了實現亂序執行,我們必須將ID流水級分為兩級。
1)發射:指令譯碼,檢查結構性冒險
2)讀取操作數:等到沒有數據冒險,隨后讀取操作數
采用計分卡的動態調度
在動態調度流水線中,所有指令都是循序通過發射級;但是,它們可能在第二級讀取操作數級停頓,或者繞過其他指令,然后進行亂序執行狀態。記分卡技術再有足夠的資源、沒有數據依賴時,允許指令亂序執行。這一功能是在CDC 6600記分卡中開發的,因此而得名。
在介紹記分卡之前先介紹一種亂序執行可能會出現的寫后讀(WAR)數據冒險問題,這在之前循序執行指令時是不會出現的,情況如下:
DIV.D FO,F2,F4 ADD.D F10,F0,F8 SUB.D F8,F8,F14ADD.D和SUB.D之間存在一種反相關性:意思就是如果亂序執行時先執行了SUB.D就會導致ADD.D數據錯誤,這就是讀后寫問題。于此類似,為避免違反輸出相關性,也必須檢查寫后寫WAW問題。而記分卡通過停頓反相關中設計的后續指令,避免了兩種冒險。
記分卡目標:通過盡早執行指令,保持每時鐘周期1條指令的執行速率。
記分卡負責:指令的發射與執行,包括所有的冒險檢測任務
要充分利用亂序執行,需要在其EX級中同時有多條指令。這一點可以通過多個功能單元、流水化功能單元或同時利用兩者來實現。
CDC6600擁有16個獨立的功能單元,包括4個浮點單元5個存儲器引用單元和7個整數運算單元。如圖:
記分卡的功能時控制指令執行。如上圖:共有兩個浮點乘法器、一個浮點除法器、一個浮點加法器和一個整數單元。一組總線(兩個輸入和一個輸出)充當一組功能單元。
每條指令進入記分卡,都會構建一條數據相關性記錄;并由記分卡判斷指令什么時候能夠讀取它的操作數并開始執行。
在詳細介紹記分卡之前,我們需要知道每條指令都需要經歷四個執行步驟:
1)發射,如果指令的功能單元空閑,并且不存在冒險時,記分卡向功能單元發射指令,并更新內部數據結構。如果存在結構性冒險或WAW冒險,則指令發射停頓,在清除這些冒險之前,不會再發射其他指令。當發射級停頓時,會導致指令提取與發射之間的緩沖區填滿;如果緩沖區是擁有多條指令的隊列,則在隊列填滿后停頓。
2)讀取操作數,記分卡見時源操作數的可用性。源操作數可用時,記分卡告訴功能單元繼續從寄存器讀取操作數,并開始執行。記分卡在這一步動態解決RAW問題,可以發送指令以進行亂序執行
3)執行,功能單元接收到操作數開始執行。結果準備就緒后,通知記分卡已經完成執行
4)寫結果,記分卡知道功能單元已經完成執行,則檢查WAR冒險,并在必要時停頓正在完成的指令。
一般來說,存在以下情況時,不能允許一條正在執行的指令寫入其結果:
a. 在正在執行的指令前邊有一條指令還沒有讀取其操作數(WAR數據冒險)
b. 這些操作數之一與正在執行指令的結果是同一寄存器(WAW數據冒險)
記分卡究竟是怎么執行的呢?看一個例子:
L.D F6,34(R2) L.D F2,45(R3) MUL.D F0,F2,F4 SUB.D F8,F6,F2 DIV.D F10,F0,F6 ADD.D F6,F8,F2記分卡中的信息如下
記分卡共有3個部分:
1)指令狀態:指出指令位于4個步驟中的哪一步
2)功能單元狀態:指出功能單元FU的狀態,共有9個字段
忙:指示該單元是否繁忙。
Op:在此單元中執行的運算,如加減
Fi:目標寄存器
Fj,Fk:源寄存器標號
Qj,Qk:生成源寄存器Fj、Fk的功能單元
Rj,Rk:只是Fj,Fk已準備就緒尚未讀取的標記。在讀取操作數后將其設置為否
3)寄存器結果狀態:如果一條活動指令以該寄存器為目標寄存器,則指出哪個功能單元將寫入寄存器。只要沒有向該寄存器寫入的未完成指令,則將此字段設置為空。
再說明一下每個執行步驟中記分卡都需要做些什么:
記分卡利用ILP(指令級并行),在最大程度上降低因為程序數據相關導致的停頓數目。在消除停頓方面,記分卡受以下幾個因素的影響。
1)指令間可用并行數:這一因素決定了能否找到要執行的獨立命令
2)記分卡的項數:這一因素決定了流水線為了查找不相關命令可以向前查找多少條指令。這組作為潛在執行對象的指令被稱為窗口。記分卡的大小決定了窗口的大小。
3)功能單元的數目和類型:這一因素決定了結構性冒險的重要性,他可能會在使用動態調度是增加。
4)存在反相關和數據相關:它們會導致WAR和WAW停頓。
啊!睡個好覺!
總結
以上是生活随笔為你收集整理的流水线问题--计算机体系结构的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 暗黑2敲石头什么意思 《暗黑破坏神
- 下一篇: 简单理解Tomasulo算法与重加载缓冲