程式設計師的 CPU 微架構

軟體與硬體預取(software and hardware prefetching)

一次快取失誤之所以昂貴,是因為中央處理器要到「索取資料的那一刻」才發現自己需要它,然後得乾等完整的 DRAM 延遲。預取攻擊這一點:在資料「真正被需要之前」就把它取進快取,於是等程式抵達那次存取時,資料早已到達、失誤被隱藏。這就像「開始煮才去叫食材」與「事先請人提早送來」的差別。

預取有兩種。硬體預取由中央處理器裡一個叫做預取器的單元自動完成,它盯著你的記憶體存取模式,一旦偵測到規律的步幅(例如循序走訪 a[0]、a[1]、a[2],或固定步距的前進),就在需求之前先抓取接下來的列。這正是為何對一個巨大陣列做簡單循序掃描,能跑在接近記憶體頻寬、而非每一列都付一次失誤——預取器看到了模式並往前跑。軟體預取則是你(或編譯器)明確發出的一條指令,例如 gcc/clang 的 __builtin_prefetch 提示,告訴中央處理器去開始載入一個「你知道很快會用到、硬體卻無法預測」的位址——典型是鏈結結構中的下一個節點,在你真正解參考它之前先算出來。

要坦白面對限制。硬體預取器對規律步幅非常在行,卻對不規律、資料相依的存取(如指標追逐或雜湊查找)視而不見——而那恰恰是傷得最重的模式——這正是軟體預取能幫上忙之處,藉由「提早夠多」發出載入。但軟體預取是把銳利且容易誤用的工具:預取得太晚,資料還沒到;預取得太早,它可能在用之前就被擠掉;浪費地預取,你污染快取又浪費頻寬,反而更慢。它不改變你必須搬動的資料總量,只改變你「何時」搬。把手動預取當成最後手段,套在特定熱迴圈上並量測,絕不當預設。

走訪一個鏈結串列時,盲目地解參考 node->next,每個節點都是預取器無法預見的全新快取失誤。提前一個節點預取——在處理 node 之前先 __builtin_prefetch(node->next)——能把下一次失誤與當前節點的工作重疊,前提是每節點的工作量大到足以隱藏那段延遲。

在硬體預取器看不見之處(指標追逐),手動預取可能有幫助——但只在時機抓對時。

手動預取很容易弄錯、且常是淨損失——距離抓錯會浪費頻寬並污染快取。硬體預取器已經免費處理了規律步幅,所以只在不規律的熱路徑上才動用軟體預取,且永遠要做基準測試。

又稱
prefetcherdata prefetchprefetch instruction預取器預先擷取