JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

五階段管線

我們把上一篇的洗衣流水線構想落實下來:經典的五個階段、夾在中間的小門栓,以及對那些會讓流水線停頓的危障所做的第一次誠實審視。

從一個大步驟拆成五個小步驟

上一篇我們把管線化看作一條洗衣流水線:當一批衣物在烘乾時,下一批已經在清洗,於是儘管每批仍要走完「洗—烘—摺」的全部時間,各批卻能一個接一個地完成。現在我們把這件事在 CPU 上落實。經典的教學設計——也就是 RISC-V 與 MIPS 教科書採用的那套——把執行一條指令的工作切成正好五個階段,每個階段在一個時脈週期裡做好一件乾淨俐落的工作。這五個階段叫做 IF、ID、EX、MEM、WB,一旦你知道每個在做什麼,整條管線讀起來就像一條你可以沿著走的裝配線。

  1. IF——取指令:依程式計數器裡的位址,從指令記憶體讀出指令,並把 PC 推進到指向下一條。
  2. ID——解碼:判讀這是哪條指令,並從暫存器檔案讀出它的來源運算元。
  3. EX——執行:由ALU做算術——一次加法、一次比較,或是算出記憶體位址。
  4. MEM——存取記憶體:載入或儲存指令在此讀寫資料記憶體;不碰記憶體的指令則直接通過。
  5. WB——寫回:把結果寫回目的暫存器,好讓後面的指令能讀到它。

為什麼偏偏是這五個?因為一旦你把記憶體那段較慢的工作、以及最後寫回暫存器這一步各自獨立出來,它們就很自然地從你先前認識的指令週期——取指、解碼、執行——衍生出來。一條單純的暫存器對暫存器加法,即使它的 MEM 階段什麼也沒做,仍要走完全部五個階段;讓每條指令長度一致,正是它們能齊步前進的關鍵。這份一致性是載入—儲存型 RISC 設計刻意給的禮物,也正是這些指令集架構能如此乾淨地管線化的原因。

管線暫存器:階段之間的門栓

接下來這段,是讓洗衣流水線在矽晶片上真正運作的關鍵。每兩個階段之間都坐著一排叫做管線暫存器的門栓。在每個時脈上升緣,某一階段的工作會被擷取進它前方的管線暫存器,而這份凍結的快照便成為下一週期下一階段的輸入。所以這些暫存器就像洗衣流水線上的隔板——那些托住半成品衣物的托盤,好讓前一台洗衣機騰出手來開始洗下一批。

每個管線暫存器都得攜帶後續階段還會用到的一切,而不只是資料。IF/ID 暫存器存放取回的指令位元。ID/EX 暫存器存放兩個運算元的值、解碼後的控制訊號,以及目的暫存器編號。EX/MEM 暫存器攜帶 ALU 的結果,MEM/WB 暫存器攜帶即將寫回的值。最容易被忘記的就是目的暫存器編號——它必須從 ID 一路搭便車到 WB,這樣四個週期後,結果才會落到正確的地方。

追蹤五條指令走下流水線

想像五條彼此獨立的指令,每個週期進來一條。到了第 5 個週期,管線就裝滿了:五個階段同時都在忙,每個處理一條不同的指令。這正是重點所在——滿載管線的穩定狀態下,每個週期都會退役一條完成的指令。把它畫成圖,那道階梯就一目了然,值得你盯著看到那條對角線在腦中「卡」進位置為止。

cycle:    1    2    3    4    5    6    7    8    9
         ---------------------------------------------
  i1 :   IF   ID   EX   MEM  WB
  i2 :        IF   ID   EX   MEM  WB
  i3 :             IF   ID   EX   MEM  WB
  i4 :                  IF   ID   EX   MEM  WB
  i5 :                       IF   ID   EX   MEM  WB

  steady state (cycles 5..5): all 5 stages busy at once.
  latency of one instruction : still 5 cycles (unchanged)
  throughput once full       : 1 instruction finished per cycle
管線的階梯圖:每條指令從頭到尾仍要 5 個週期,但流水線一旦裝滿,每個週期就有一條完成。

從這張圖讀出兩個數字,因為把它們搞混正是初學者的經典錯誤。任何單一指令的延遲仍是五個週期——管線化沒讓 i1 早一丁點完成;它從不改善單一指令的延遲。改變的是吞吐量管線吞吐量):流水線裝滿後,每個週期就會冒出一個結果,而不是每五個週期才一個。跑一支有 N 條指令的長程式,你大約花 N + 4 個週期,而非 5N——幾乎是五倍的加速,那個 +4 則是裝滿管線的一次性成本。

可是指令很少這麼有禮貌

那道乾淨的階梯,假設了每條指令都與其他指令彼此獨立。真實程式可沒這麼仁慈:一條指令常常需要某個值,而前面那條指令還沒把它生出來。這些衝突叫做危障管線危障),也正是接下來三篇指南存在的理由。危障分三大家族,現在就先按名認識它們是值得的,儘管我們稍後才會醫治它們。

結構危障是兩條指令在同一週期想用同一塊硬體——就像一台洗衣機卻有兩批衣物同時要用它。控制危障來自分支:在分支於 EX 階段定案之前,管線並不真正知道下一條該取哪條指令,於是它可能已經開始往錯的那條路取指。而日常最常見的麻煩製造者——資料危障——是一種「寫後讀」相依:一條指令想在 ID 階段讀某個暫存器,而負責寫它的那條指令卻還只走到 EX 或 MEM、尚未抵達 WB。所需的值是存在的,只是還卡在管線中段。

看一個你會不斷遇到、又令人頭痛的小案例——載入—使用危障。假設 `lw x1, 0(x2)` 緊接著 `add x3, x1, x4`。載入指令要到它的 MEM 階段結束時,才從資料記憶體拿回值,但加法指令在它的 EX 階段就想要 x1,而那個時點早了一個週期。再聰明的接線也無法交出一個尚不存在的值,所以單單這個危障無法被完全藏起來——管線必須插入至少一個浪費掉的週期,也就是一次停頓,我們會在第 4 篇看到這個載入指令為何是頑固的例外。

這對 CPI 的影響——以及那個誠實的取捨

回想效能的鐵律:執行時間 = 指令數 x CPI x 週期時間。五階段管線的夢想是 CPI 等於 1——穩定狀態下每個週期完成一條指令。危障會稍稍破壞這個夢。硬體每插入一次停頓,就多出一個浪費的週期,把真實觀測到的 CPI 推到 1 以上。我們稱之為有效 CPI:理想的 1,加上每條指令平均造成的停頓週期數。若 20% 的指令是載入,而其中四分之一逼出一個週期的停頓,你平均每條指令就多付 0.05 個週期——有效 CPI 是 1.05。

那為什麼不把工作切成十段、二十段,好換更高的時脈?這就是管線深度取捨,而它確實是雙面刃。較深的管線每階段做的事更少,所以每階段更快、時脈能跳得更高——這是真的。但好處會縮水、代價會膨脹。固定的門栓開銷在每個更短的階段裡佔的比例更大,於是你並不能從更高的時脈率拿到全額的加速。更糟的是,一次分支預測錯誤或一次停頓如今會浪費更多飛行中的週期,因為要丟掉的半成品指令變多了。