預取(prefetching)
好的服務生注意到你的水喝到一半,就在你開口前先幫你續上——於是你從不必等。預取就是快取扮演這位細心的服務生:它試圖猜出程式很快會需要哪些資料,並在程式實際開口「之前」就把它抓進快取,好讓本該是慢未命中的,到真正存取時已變成快命中。
這種猜測利用的是和快取相同的區域性。最簡單也最常見的是硬體串流/跨步預取器:它觀察被存取的位址、偵測規律的模式(例如每次存取都比上次後移 64 位元組——對陣列的單位跨步),並推測性地把下一條或往前好幾條列抓進來。軟體預取是另一種形式:編譯器或程式設計者插入明確的預取指令(提示說「請開始抓位址 X,我很快會需要它」),針對硬體不易察覺的模式。無論哪種,目標都是把漫長的記憶體延遲和有用的計算重疊,好讓資料在被需要時已經到了。
預取對可預測的循序存取(串流走過陣列)最有效,這也是為何向前掃陣列即使底層延遲很高,仍能跑得接近記憶體頻寬的速度。但它是推測性的賭注,誠實面對很重要:猜錯會浪費記憶體頻寬去抓從不使用的資料,甚至為騰位而淘汰有用資料(快取污染),所以過度積極的預取器反而可能更慢。它也救不了真正不規則、依賴資料的存取——追逐四散的指標,下一個位址要等當前載入完成才知道,使跨步預測失效。預取藉由在第一次使用前把資料拉進來而打擊強制未命中,但它是放大良好區域性,而非無中生有地創造它。
對百萬元素的陣列求和,跨步預取器看出單位跨步模式、在迴圈前方先抓列,於是每條列在被走到時已在快取裡——迴圈跑得接近記憶體頻寬。同一個預取器對鏈結串列的走訪毫無作用,因為每個下一位址要等當前節點載入才知道。
在資料被要求前先抓,把未來的未命中變成命中——對可預測的跨步極有效。
預取是會反噬的推測性賭注:猜錯會浪費頻寬,並可能因淘汰有用資料而污染快取、使程式碼變慢。而對下一位址要等當前載入回來才知道的不規則、追指標式存取,它無法相救。