程式設計師的 CPU 微架構

快取列(cache line)

當你向圖書館員要一頁時,他不會把那一頁撕下來遞給你——他會把整本書搬到你的桌上,因為「取」這個動作才是昂貴的部分,而你大概也會想看附近的頁。快取的運作一模一樣。當中央處理器需要某個尚未被快取的位元組時,它從不只取那一個位元組,而是取它周圍一整塊固定大小的記憶體,稱為快取列。

在幾乎每一顆現代 x86 與 ARM 晶片上,快取列都是 64 位元組,並對齊到記憶體中的 64 位元組邊界(所以含有位址 0x1008 的那一列橫跨 0x1000 到 0x103F)。這個 64 位元組的區塊,是 DRAM 與快取之間、以及各快取層之間搬動的最小單位——根本沒有「只快取單一位元組」這回事。讀一個 int(4 位元組),鄰近的 60 個位元組就免費地一起搭便車進入 L1。這就是空間區域性背後的機制:對附近位址接下來幾次存取的成本,第一次失誤時就已經付清了。

由此直接導出兩個後果。第一,把你的資料佈局成「會一起用的東西落在同一列(或連續的列)」,在第一次觸碰後便幾乎免費可存取——這是資料導向設計的核心。第二,快取列也是核心之間維持一致性的單位,因此兩個執行緒各自寫入兩個不同變數、卻碰巧共用同一個 64 位元組的列時,它們就會為那一列爭搶,即使從未碰到同一個位元組。這種意外的競爭就是偽共享(false sharing),而它在原始碼裡是看不見的。

struct Hot { int a; int b; }; 讀取 h.a 也會把 h.b 拉進快取——兩者同住在一個 64 位元組的列裡。但若兩個執行緒各自猛打放在同一列裡的兩個互不相關的計數器,效能就會因偽共享而崩潰;用 alignas(64) 把它們撐開到不同列。

這個列既是傳輸的單位、也是競爭的單位:鄰居免費載入,但由不同核心寫入的鄰居會相撞。

64 位元組壓倒性地常見,卻不被任何可移植的東西所保證——在重要場合請於執行期查詢;有些晶片用 128 位元組的列、或成對拉兩列,在程式裡寫死一個固定大小就是個臭蟲。

又称
cache block64-byte line快取行快取區塊