越過「每週期一條指令」
在上一級裡,你看著管線把一支程式的「洗衣」變成一條流水線:當一條指令在執行階段洗著,下一條已在解碼,再下一條在取指。那是個漂亮的把戲,但請注意它安靜的天花板——簡單的管線每個週期仍然只啟動剛好一條新指令。最好的情況下,它每週期退出一條,所以它的 CPI 觸底在 1。整個這一級,就是要打穿那層地板:打造一顆 CPI 能掉到 *1 以下* 的核心,也就是它真的每週期完成不只一條指令。
我們開採的這項資源有個名字:指令層級平行,也就是 ILP。它的概念是:在一支普通的、單執行緒的程式裡——沒有多執行緒、沒有額外核心——通常會有彼此靠近的好幾條指令互不相依,因此原則上可以在同一時刻運行。看這三行:`a = b + c`、`d = e + f`、`g = a + d`。前兩行毫無瓜葛;一顆聰明的核心可以同時算出這兩個加法,再做第三個。這種潛在的可重疊能力就是 ILP,而這一級接下來談的,便是去尋找並利用它的那套機器。
超純量核心:更多車道,而非更快的時脈
利用 ILP 最直接的方法,就是把機器加寬。一顆超純量核心擁有好幾個並排的執行單元——比如兩個 ALU、一個載入/儲存單元、一個乘法器——並且每個週期取指、解碼、發射的不是一條指令,而是一小束。想像你熟悉的那條五階段管線,但加寬成兩或四條車道,就像超市裡一條單線結帳道,變成了一整排四台收銀機。如果接下來幾條指令剛好彼此獨立,核心就在同一週期把它們分派進不同的單元,並一起完成。
這正是對百萬赫茲迷思的誠實回答。人們曾以為機器更快不過就是時脈速率更高,但效能鐵律(執行時間 = 指令數 x CPI x 週期時間)擺明了有三個旋鈕,不只一個。超純量核心根本不碰時脈;它攻擊的是 CPI 這一項,每一拍做更多工作。兩顆跑在同樣吉赫茲的核心,真實速度可以天差地遠,因為其中一顆每週期退出四條指令,另一顆只退出一條。在這裡,撥動的桿子是寬度,不是頻率。
現在值得認識一個對手設計:VLIW,也就是超長指令字。超純量核心是在執行期、用硬體去算出哪些指令能湊在一起——那是燒電又燒矽的昂貴邏輯。VLIW 則把這份工作推給編譯器:它事先把彼此獨立的運算打包進一條寬寬的指令,硬體只要盲目地把這束裡的每一格平行射出,完全不需要排程邏輯。這筆交易鮮明而誠實:VLIW 硬體較簡單也較涼,但編譯器必須在建置期預測「硬體本來會在執行期學到的東西」——而它無法預見一次快取未命中或一個分支的結果,所以它的束裡常被填進浪費的空指令。這正是為什麼主流 CPU 走了超純量,而 VLIW 主要活在 DSP 與某些加速器裡。
為什麼順序非彎不可:相依性
把機器加寬,只有在接下來幾條指令真的彼此獨立時才有用——而它們往往不是。誠實的障礙叫做相依性。一個真正的相依(寫後讀,也就是你在管線冒險裡見過的 RAW)是根本性的:如果指令 B 需要指令 A 產出的值,B 就是不能在 A 的結果存在之前開始。沒有任何硬體能把它許願許掉。但第二種更狡猾的障礙,卻一點也不根本。
看這段追蹤,編譯器要兩個毫不相關的計算,都去使用暫存器 x5:
I1: x5 = x6 + x7 # produces a result in x5 I2: x8 = x5 + x9 # TRUE dependence: really needs I1's x5 I3: x5 = x10 + x11 # reuses the NAME x5 - but new, unrelated work I4: x12 = x5 + x13 # depends on I3's x5, not I1's I3 must wait for I1/I2 only because both grabbed the name 'x5'. The VALUES are independent; only the NAME collides. -> false dependence
I3 把 x5 這個名字重用在全新的工作上,那工作跟 I1 毫無關係。硬體一看到同一個暫存器編號,便擔心衝突而把它們序列化——即使實際的值彼此獨立。這就是假相依:一次並非由資料、而是由暫存器名字不夠用所造成的停頓。下一篇導覽會完整展開的解法是暫存器重新命名:核心維護一大池隱藏的實體暫存器,悄悄把每一次對 x5 的新寫入,對映到一個不同的實體格子,於是 I1 的 x5 與 I3 的 x5 不再相撞,能夠平行運行。架構只承諾少少幾個架構暫存器;微架構卻偷偷提供了多得多的暫存器。
亂著做,卻照順序上菜
即使有了重新命名,一台嚴格照順序的機器,只要下一條指令正在等待——也許在等一次緩慢的記憶體載入——就會停頓,而後頭一堆完全就緒的指令只能閒坐。解法是亂序執行:核心啟動的,是運算元已經就緒的那條指令,而不是程式順序上的下一條。把它想成一間忙碌的廚房,誰的食材都備齊了就先煮誰,而不是死板地先到先做。讓這件事成立的機器——保留站與 Tomasulo 演算法——是第 3 篇導覽的主題;現在只要先握住「指令一能跑就發動」這幅畫面。
這種「想做就做/照順序提交」的分離,是承重的關鍵概念,而且帶著一股熟悉的味道:指令集架構這份合約說,結果會一個接一個出現,而微架構則可以在底下任意作弊,只要沒人看得出來就行。順序提交正是維繫這個謊言誠實的東西——它是那道接縫,把內部狂野的重排,重新縫回程式設計師被許諾的那個循序故事裡。這一級的第 2 篇,整篇都在講那道接縫如何撐住。
猜測未來,以及分享核心
通往深層 ILP,還有一個障礙:分支。大約每五、六條指令就有一條分支,而核心要到很晚才能知道它往哪走。乾等會把又深又寬的整條管線抽乾。所以現代核心會推測:一個分支預測器對「這條路岔向哪邊」下注,機器便沿著猜中的路徑往前衝,在分支甚至還沒定案之前,就先執行了幾十條指令。這是在一個你還沒抵達的路口,賭你會往哪邊轉。猜對了(好的預測器猜對率遠超過 95%)你毫無損失;猜錯了,核心就得把整條推測路徑全丟掉、重來——這正是為什麼重排序緩衝器要讓推測的工作維持未提交,直到那一注塵埃落定。
預測器一代比一代精明:兩位元預測器給每條分支一點遲滯,讓單一次意外不致翻轉它的猜測;關聯式預測器則注意到分支常取決於最近其他分支怎麼走;而錦標賽預測器同時運行好幾套方案,並針對每條分支學會該相信哪一套——再配上一個分支目標緩衝器,把「跳了會落到哪裡」快取起來,讓核心連先解碼都不必。這一級的第 4 篇會把這些拆開細看。關鍵的誠實是:這一切都是精密的猜測,而猜錯的復原代價是真實的。
即便一顆出色的亂序核心,也未必總能找到足夠多的獨立指令去填滿它寬闊的後端——有時程式就只是一串環環相扣的相依步驟,執行單元只能閒著。一個優雅的補救是把浪費掉的格子分享給第二支程式:同時多執行緒(Intel 稱之為超執行緒)讓兩條執行緒在同一週期發射進同一顆核心,於是當一條執行緒卡在快取未命中時,另一條仍讓那些單元忙碌。但這已逼近這一級誠實的結語:任何單一指令流所含的 ILP,有一道硬天花板。越過寬度只有幾的核心後,多塞電晶體幾乎買不到什麼——這就是 ILP 的極限——而這道牆,連同「免費衝頻率」時代的終結,正是業界轉向多核心的緣由。而讓這顆核心變快的那個推測本身,也正是 Spectre 一類旁通道藏身之處——猜錯的指令會在快取裡留下淡淡的腳印。這一級的第 5 篇,便收在這個轉折上。