Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 26 · 臺南 AQI 21 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

CPU 與指令執行

CPU 與指令執行(進階):亂序、重新命名與 Tomasulo

從 ISA 與微架構的分野出發,拆解暫存器重新命名、保留站、重排序緩衝區與分支預測,看現代處理器如何「內部瘋狂亂序、外部嚴格有序」。

一條 add 指令到了現代 CPU 手裡,為什麼會被「拆開」再「重排」?

入門篇裡,我們把 CPU 想成一條乖巧的生產線:依程式計數器(PC, Program Counter)取出指令、解碼、執行,一條接一條,井然有序。這個畫面很適合建立直覺,卻和今天桌上型晶片內部真正發生的事相去甚遠。

真實情況更像一座吵雜的調度場:你寫下的指令一進處理器就被拆解成更細的微操作(micro-op)、貼上臨時編號、丟進一個共享的等待池,誰的輸入先備齊誰就先跑——跑完的順序甚至可能和你寫的順序完全不同。最後再有一個專門的單位負責「假裝一切都是照順序發生的」,把結果按程式原本的次序提交出去。

這篇進階文章要回答一個核心問題:現代 CPU 如何在「對外看起來嚴格照順序執行」與「對內瘋狂亂序、平行、推測」之間取得平衡? 我們會從指令集架構(ISA)與微架構的分野出發,一路走到暫存器重新命名(register renaming)、Tomasulo 演算法、重排序緩衝區(ROB),以及分支預測器的內部結構。

兩層抽象:ISA 與微架構

入門篇談的「機器語言」其實對應到一個正式名詞:指令集架構(ISA, Instruction Set Architecture)。它是硬體與軟體之間的契約,規定了有哪些指令、有幾個程式可見暫存器、記憶體如何定址、例外如何處理。x86-64、ARMv8(AArch64)、RISC-V 都是 ISA。

CPU 與指令執行進階概念示意圖

關鍵在於:ISA 只規定「行為」,不規定「怎麼做到」。同一套 x86-64 ISA,Intel 與 AMD 的實作天差地別;同一家公司不同世代的晶片實作也完全不同。這個「怎麼做到」的層次,叫做微架構(microarchitecture)

層次 規定什麼 範例
ISA 程式設計師看得到的行為契約 x86-64、ARMv8、RISC-V
微架構 硬體如何實現該契約 Intel Golden Cove、Apple Firestorm、AMD Zen 4

這個分野之所以重要,是因為它讓底下的工程師可以為所欲為:只要對外行為符合 ISA 契約,內部要拆指令、亂序、推測、快取都隨意。學習者要建立的核心心智模型是——ISA 是「程式語意」,微架構是「執行策略」,兩者解耦

CISC 的指令會先被翻成 micro-op

x86 是典型的複雜指令集(CISC),一條指令可能同時做「從記憶體讀、做運算、寫回記憶體」好幾件事。現代 x86 處理器並不直接執行這種複雜指令,而是在前端(front-end)先把它解碼成一條或多條精簡的微操作(micro-op / µop),這些 µop 長得很像精簡指令集(RISC)的單純指令。

x86 指令:        add  [rax], rbx     ; 把 rbx 加到記憶體 [rax] 的內容

解碼後的 µop:    load   t1 ← [rax]   ; 讀記憶體到臨時值
                add    t2 ← t1, rbx  ; 相加
                store  [rax] ← t2    ; 寫回記憶體

換句話說,現代「CISC」處理器的執行核心其實是一個 RISC 式的引擎。理解這點,後面所有亂序機制就都建立在「處理器真正調度的是 µop,不是你寫的指令」這個前提上。

真相位元:為什麼要「重新命名暫存器」

入門篇提過資料危障(data hazard):後一條指令需要前一條的結果。但還有一類危障更隱蔽,它根本不是真正的相依,而是因為暫存器名字不夠用造成的假相依。看這段:

(1) add  r1 ← r2, r3
(2) mul  r4 ← r1, r5     ; 真相依:要等 (1) 的 r1     —— RAW
(3) add  r1 ← r6, r7     ; r1 被重複使用,和 (1) 撞名 —— WAW
(4) sub  r8 ← r1, r9     ; 用的是 (3) 的 r1,不是 (1)  —— WAR / RAW
  • RAW(讀後寫,Read-After-Write):(2) 讀 (1) 寫的 r1。這是真相依(true dependency),無法消除,本質上資料就是要流過去。
  • WAW(寫後寫)WAR(寫後讀):(3) 又寫了 r1,純粹是因為這顆 ISA 只有 16 個架構暫存器(architectural register),編譯器被迫重複使用同一個名字。這兩者叫假相依(false / name dependency)——資料其實沒有流動關係,只是名字撞了。

解法是暫存器重新命名(register renaming):處理器內部其實有遠多於架構暫存器的實體暫存器(physical register)(現代核心常有 100~200 個以上)。每次有指令「寫」一個架構暫存器,就分配一個全新的實體暫存器給它,並更新一張對照表(Register Alias Table, RAT)。

重新命名後(p 開頭是實體暫存器):
(1) add  p10 ← p2, p3     ; r1 映射到 p10
(2) mul  p11 ← p10, p5    ; 讀 p10(真相依保留)
(3) add  p12 ← p6, p7     ; r1 改映射到 p12 —— 和 (1) 的 p10 完全無關了!
(4) sub  p13 ← p12, p9    ; 讀 p12

WAW 與 WAR 假相依瞬間消失,因為 (1) 和 (3) 現在寫的是不同的實體暫存器。這就是亂序執行能成立的關鍵前置處理:先把名字洗乾淨,剩下的只有真正不可違背的資料流。

Tomasulo 演算法:讓「準備好的」先跑

名字洗乾淨後,µop 被送進保留站(reservation station)——一個等待池。每個 µop 在站裡等它的運算元(operand)到齊;一旦兩個輸入都備妥、又有空閒的功能單元(functional unit),它就被發射(issue)出去執行,完全不管它在程式裡排第幾

這套機制源自 1967 年 IBM System/360 Model 91 的 Robert Tomasulo,至今仍是所有亂序處理器的骨架。它的精妙在於用一條共同資料匯流排(CDB, Common Data Bus) 廣播結果:任何功能單元算完,就把「我是 p11、我的值是 42」廣播到所有保留站;正在等 p11 的 µop 一聽到,立刻抓值、湊齊運算元、準備發射。

保留站快照(某個時脈週期):
┌────┬────────┬───────────┬───────────┐
│ 站 │ 操作   │ 運算元1    │ 運算元2    │
├────┼────────┼───────────┼───────────┤
│ RS1│ mul    │ p10 ✓ =7  │ p5  ✓ =2  │ ← 都到齊,可發射
│ RS2│ add    │ 等 p11    │ p9  ✓ =1  │ ← 卡在 p11,先等
│ RS3│ sub    │ p12 ✓ =5  │ p9  ✓ =1  │ ← 都到齊,可發射
└────┴────────┴───────────┴───────────┘
RS1 與 RS3 可同時發射到兩個 ALU;RS2 等 CDB 廣播 p11 後才動。

注意 RS3(程式中較後面的 sub)可以比 RS2(較前面的 add)先執行——這就是亂序執行(out-of-order execution) 的字面意思。處理器讓資料流(dataflow)決定執行順序,而非程式碼順序。

對外保持秩序:重排序緩衝區(ROB)

問題來了:如果指令亂序完成,萬一中間有一條觸發了例外(exception)、或某個分支預測錯了,怎麼辦?我們不能讓「程式順序上還沒輪到、卻已經搶先算完並改了記憶體」的指令造成不可逆的後果。

解法是重排序緩衝區(ROB, Reorder Buffer):一個 FIFO 佇列,µop 進入執行前按程式順序在 ROB 尾端登記一個位置,但完成順序任意。一條 µop 即使算完,也只是把結果暫放在自己的 ROB 條目裡,標記為「完成」但尚未提交

只有當一條 µop 走到 ROB 的最前端(head)、且確認它前面所有指令都已正確提交時,它才被退役(retire / commit)——這一刻它的結果才正式寫進架構狀態(程式可見暫存器、記憶體)。退役嚴格按程式順序進行。

亂序執行,順序退役:

執行完成的時間順序:  C  A  D  B   (亂)
                     ↓
ROB(按程式順序排隊): A → B → C → D
退役順序:             A  B  C  D   (照程式順序,一個都不能跳)

這個設計達成一個漂亮的雙重目標:內部極致亂序求效能,外部嚴格順序求正確。它也讓「精確例外(precise exception)」成為可能——若 B 發生分頁錯誤(page fault),處理器只要丟棄 ROB 中 B 之後(C、D)尚未退役的所有結果,狀態就乾淨地停在「A 已完成、B 出錯」,彷彿從未亂序過。

動手算一下:亂序到底快多少

考慮一段有長延遲載入的程式碼。假設 load 要 4 個週期(快取失誤更久),ALU 運算 1 個週期:

(1) load r1 ← [r2]      ; 4 週期
(2) add  r3 ← r1, r4    ; 真相依於 (1),必須等
(3) mul  r5 ← r6, r7    ; 與 (1)(2) 完全無關
(4) sub  r8 ← r6, r9    ; 與前面都無關

順序執行(in-order):嚴格照順序,(2) 卡住等 (1) 的 4 個週期時,(3)(4) 只能乾等。

週期:   1   2   3   4   5   6   7
(1)    [--- load 4 週期 ---]
(2)                        add
(3)                             mul
(4)                                  sub
總計約 7 個週期

亂序執行(out-of-order):(2) 在等 (1) 時,獨立的 (3)(4) 趁機填補空檔先跑:

週期:   1   2   3   4   5
(1)    [--- load 4 週期 ---]
(3)     mul
(4)         sub
(2)                        add
總計約 5 個週期

加速並非來自指令變快,而是來自把因相依而閒置的空檔(bubble)用獨立指令填滿。能填多滿,取決於程式裡有多少指令級平行度(ILP, Instruction-Level Parallelism)可挖。這也解釋了為何亂序窗口(ROB 大小)是現代核心的關鍵規格——窗口越大,越能往後看到更多獨立指令來填空。

分支預測:賭對了免費,賭錯了很貴

入門篇提過控制危障:在分支(branch)解出方向前,不知道下一條該取哪裡。亂序機器的管線又深又寬,一次預測失敗要清掉的 µop 多達數十條,懲罰極重。所以現代 CPU 的分支預測器(branch predictor) 做得極其精巧,準確率常達 95% 以上。

最基礎的構件是 2 位元飽和計數器(2-bit saturating counter)。它用四個狀態記錄「最近這個分支傾向跳或不跳」,避免單次反例就立刻翻盤:

狀態機(每個分支位址配一個計數器):

  強烈不跳 ⇄ 弱不跳 ⇄ 弱跳 ⇄ 強烈跳
   (00)      (01)    (10)    (11)
   預測:不跳         |  預測:跳

跳了 → 往右移一格(更傾向跳)
不跳 → 往左移一格(更傾向不跳)

關鍵設計:要連續猜錯兩次才會改變「跳/不跳」的預測方向。對一個跑 1000 次、只有最後一次跳出的迴圈,2 位元計數器全程只錯 1 次(最後跳出那次),遠優於「看上一次」的 1 位元預測器。

現代預測器更進一步引入歷史關聯:用一個全域歷史暫存器(GHR, Global History Register)記錄最近 N 個分支的實際走向,把這段歷史與分支位址雜湊後,去查一張型樣歷史表(PHT, Pattern History Table)。這樣即使是相關聯的分支(例如 if (x>0) ... if (x>0) ...)也能被準確預測。實務上的 TAGE、感知器(perceptron)預測器都屬此類進階設計。

預測對了,目標位址由分支目標緩衝區(BTB, Branch Target Buffer) 提供,管線無縫往下走;預測錯了,沿錯誤路徑推測執行(speculative execution)的 µop 全部從 ROB 中作廢,回到正確路徑重來——這就是入門篇提到的清空(flush)懲罰。

重點回顧

  • ISA 與微架構解耦:ISA 是對外的行為契約(x86、ARM、RISC-V),微架構是內部實作策略;同一 ISA 可有天差地別的實作。
  • 現代處理器把指令(尤其 CISC)拆成 µop 來調度,執行核心本質上是 RISC 式引擎。
  • 暫存器重新命名消除 WAW/WAR 假相依,把問題化約為不可違背的真資料流,是亂序執行的前提。
  • Tomasulo + 保留站 讓運算元到齊的 µop 先發射,由資料流而非程式順序決定執行次序;ROB 則保證退役嚴格按程式順序,達成「內部亂序、外部有序、例外精確」。
  • 分支預測器(2 位元飽和計數器、GHR+PHT、BTB)讓深管線在面對控制危障時仍能高吞吐運行,預測錯誤的代價是管線清空。

深入探討(研究所視角)

把上述機制串起來,現代高效能核心的真實面貌是:前端(取指、分支預測、解碼成 µop)→ 重新命名(RAT 分配實體暫存器)→ 亂序引擎(保留站、ROB、多個功能單元)→ 退役(按序提交)。值得在研究所層次延伸的幾個方向:

記憶體亂序與一致性(memory consistency)。 暫存器的相依靠重新命名解得很乾淨,但記憶體位址要到執行期才算得出來,使得 load/store 之間的相依變成模糊問題。處理器用載入/儲存佇列(load/store queue)記憶體相依預測(memory dependence prediction) 來推測「這個 load 是否與前面某個未完成的 store 撞址」,猜錯就得回滾。更深一層,不同 ISA 定義了不同的記憶體模型(memory model):x86 採較強的 TSO(Total Store Order),ARM/RISC-V 採較弱的鬆散順序(weak ordering),這直接影響多執行緒程式何時需要插入記憶體屏障(memory barrier / fence)。這是並行程式設計正確性的硬體根基,連結到 C++/Java 的記憶體模型與 std::atomic 的記憶體序語意。

超純量與 SMT。 入門篇提到超純量(superscalar)可在單週期發射多條指令;當單執行緒的 ILP 被榨乾、功能單元仍有空閒時,同時多執行緒(SMT, Simultaneous Multithreading;Intel 稱 Hyper-Threading) 讓兩個硬體執行緒共享同一套執行資源,用執行緒級平行(TLP)填補 ILP 留下的空檔。代價是共享資源(快取、保留站)的競爭,以及——如下——安全邊界的模糊。

推測執行的安全代價,再訪。 入門篇點到 Spectre/Meltdown。從本篇的機制看會更透徹:問題正出在「推測執行的 µop 即使最終在 ROB 被作廢、架構狀態完好回滾,它在快取等微架構結構留下的痕跡卻不會回滾」。攻擊者誘導處理器沿錯誤路徑推測載入機密資料、用它索引一塊陣列,再透過快取時序(cache timing)側通道把值還原出來。這揭示一個深刻的張力:ISA 契約只保證架構狀態的正確,卻從未承諾微架構狀態不洩漏資訊。後續的緩解(如推測載入硬化、站點隔離)與新研究方向(不可見推測, invisible speculation)都圍繞這條縫隙。理解這類議題的價值在於建立正確的系統設計與防禦觀念,相關探究應於合法、受控的研究環境中進行。

效能上限與 Amdahl 視角。 亂序窗口、發射寬度、預測準確率三者共同決定可達 IPC,但回報遞減:把 ROB 從 200 加到 400 條目,能多挖到的獨立指令越來越少(程式內在 ILP 有限)。這把效能戰場從「單核更寬更深」推向「多核 + 向量(SIMD) + 異質運算(GPU/NPU)」。你會發現入門篇結尾那句話再次應驗——「如何讓更多單元同時忙碌、又維持正確性」這個命題,從單條管線、到亂序窗口、到多核一致性、到資料中心級的分散式系統,只是換了尺度反覆登場。掌握本篇的 µop 調度與 ROB 退役模型,等於拿到了一把能貫穿所有尺度去思考「平行 vs. 正確」的鑰匙。

AI 共讀助教正在陪你讀:CPU 與指令執行(進階):亂序、重新命名與 Tomasulo
嗨!我是這篇文章的共讀助教,只根據〈CPU 與指令執行(進階):亂序、重新命名與 Tomasulo〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。