我們蓋了什麼,以及它逼出的問題
退一步看看這四篇導覽組裝出的引擎。一個超純量前端每個週期取出並發射好幾條指令。亂序執行讓任何運算元就緒的指令提早跑,而重排序緩衝區仍照程式順序退役它們,使可見的結果保持循序。暫存器重新命名化解了那些本會把一切序列化的偽相依,而由一個好分支預測器引導的推測執行,在我們還不知道分支是否成立之前就先衝過去。每個花招只有一個目標:找出更多指令層級平行度——同一個週期內能做的、彼此獨立的工。
於是這一整階一直在鋪陳的誠實問題來了:如果我們不斷把機器加寬——八路、十六路、越來越大的指令視窗——效能會一直爬升嗎?不會,而且差得不只一點。一個普通程式內部「可用的」平行度是有限的,遠在我們用完電晶體之前,就先用完了能餵給它們的、彼此獨立的指令。這道天花板有個名字:ILP 的極限。
為什麼平行度會用完
三道牆同時逼近。第一,真資料相依:如果指令 B 真的需要 A 算出的值,再多的聰明也不能讓 B 搶在 A 前面跑——重新命名拿掉的是偽相依,永遠拿不掉真的;而一長串相依運算就是一條繩,機器只能一個結一個結地往上爬。第二,分支:真實程式碼每五、六條指令就分支一次,要看得夠遠以找到獨立的工,處理器必須連續猜對許多分支。即便一個 95% 準的預測器,自乘十次後,從頭到尾全對的機率也只剩約 60%——所以推測真正有用的視窗,比看起來短。
第三,也最具決定性,成本長得比好處快。要榨出更多平行度,你需要更大的重排序緩衝區、更多保留站、更多重新命名暫存器、暫存器檔上更多讀寫埠,以及更寬的喚醒邏輯——而其中好幾種結構,大致隨發射寬度的「平方」成長。你花掉多得多的矽與能量,只哄出一點點速度。這就是 ILP 的功耗與複雜度代價:經典研究發現,超過大約四路發射之後,真實程式給的報酬急遽遞減。一台多數時間閒著的更寬機器,不過是一台昂貴的暖爐。
兩道出路:先更多執行緒,再更多核心
如果一條指令流餵不飽一台寬機器,何不用兩條流去餵它?這就是同步多執行緒(SMT)——也就是 Intel 的超執行緒。核心本來就有比單一執行緒通常填得滿還多的執行槽,於是 SMT 讓兩條(或更多)執行緒在同一個週期發射進同一個後端。當一條執行緒因快取未命中而停頓,另一條讓那些單元保持溫熱。這是個節儉的花招——幾乎不花額外的矽,因為它重用了你已經蓋好的引擎——但它是「共享」而非「倍增」資源,所以兩條執行緒很少跑出兩倍快,甚至可能因為搶快取而互相拖慢。
更大的那道門,是產業大約在 2005 年真正走過去的:別再把單一核心越做越寬,而是把「好幾顆完整的核心」放上晶片——也就是多核心處理器。這不是流行,是被逼的。Dennard 縮放——那條讓電晶體變小、每一代能在同樣功率下跑更快的美好規律——大約在 2005 年實質終結,於是再把時脈或發射寬度往上催,就會把晶片燒熔。在單一指令流的 ILP 被榨乾、功率又被封頂之後,要花掉仍在成長的電晶體預算,唯一剩下的辦法就是「複製」:更多核心,揭露執行緒層級平行度,而不是在一條執行緒上更用力地擠。
誠實的傷疤:Spectre 與推測的代價
現在來到不舒服的部分。我們在第四篇對自己講了一個安慰的故事:推測是安全的,因為當分支猜錯,機器會「壓掉」錯誤路徑上的指令——丟棄它們的暫存器、絕不提交它們,並把架構狀態回捲到彷彿它們從未跑過。可見的結果是正確的。但「從未跑過」其實是個半真半假的話。那些錯誤路徑上的指令並沒有改變任何你叫得出名字的暫存器——然而在它們短暫存活的那段時間裡,它們可以碰到快取,而被碰過的快取列,會比沒碰過的更溫熱。它們在微架構狀態裡留下了一個回捲抹不掉的腳印。
那個腳印就是一條側通道(side channel)。被壓掉的指令其結果被丟棄了,但「之後讀記憶體所花的時間」悄悄地把那條指令碰過什麼編碼了下來:快取命中很快,未命中很慢,你拿一只碼錶就能分辨。Spectre 把這一點武器化得淋漓盡致。攻擊者訓練分支預測器去猜錯,哄騙核心去推測性地讀取一個它無權使用的祕密,並讓那個祕密去左右哪一條快取列被載入。架構狀態盡責地回捲了——但攻擊者接著為一次快取計時探測計時,讀出哪一條列被加溫,一次一個位元地把祕密還原出來,直接穿過了硬體誓言要守住的保護邊界。
1. Train the predictor so a bounds check is predicted 'in range'.
2. Speculatively run: secret = array[evil_index]; // out of bounds!
3. Speculatively run: junk = probe[secret * 64]; // touches one cache line
4. CPU discovers the misprediction -> squashes 2 and 3, rolls registers back.
5. BUT probe[secret*64] is now cached. Time each probe[i]:
the one fast access reveals 'secret'. One byte leaked.Spectre 真正教我們的事
Spectre 的手足 Meltdown 範圍較窄——它利用了某家廠商的一個習慣:在權限檢查完成之前,就讓一條載入推測性地看到核心資料;一個微碼加作業系統的修補大致堵住了它。Spectre 更深也更難,因為它濫用的是「正確、照規矩來」的推測,而每一顆高效能核心都倚賴它。沒有單一乾淨的補丁;防禦是一張拼布——編譯器柵欄、在情境切換時清空預測器、在敏感程式碼裡關掉部分推測——每一招都吐回一片當初加進推測想換來的效能。這是最乾淨不過的證明:效能與安全,可以處在真正的緊張關係中。
- ILP 是有限的:真資料相依與分支限住了一條流能平行多少,而成本大致隨發射寬度的平方成長。
- 大約在 2005 年 Dennard 縮放終結,於是產業不再加寬單一核心,轉向 SMT,接著轉向多核心。
- 多核心把找平行度的活轉到了程式設計師身上,而 Amdahl 定律以循序比例封住了報酬。
- 推測會留下微架構痕跡;Spectre 透過快取計時側通道讀出那些痕跡,跨越保護邊界洩漏祕密。
退一步,看看這一整階的弧線。我們在一條普通指令流裡發現了龐大的隱藏平行度,並蓋出令人驚嘆的機器——亂序發射、重新命名、Tomasulo 演算法、重排序緩衝區、分支預測——去收割它,同時讓結果完美地保持有序。然後我們撞上了牆:那份平行度是有限的,那台機器是昂貴的,而這正「為什麼」這道階梯接下來幾階要轉向眾多核心、GPU 與加速器。我們以一個誠實的音符作結:那為我們贏得數十年速度的聰明,正是 Spectre 反過來對付我們的同一份聰明。偉大的架構不只是把它做快——而是理解、並承擔,「快」真正的代價。