TLB 失誤與分頁表走訪(the page-walk)
當 TLB 沒有握著你需要的轉譯時,答案並未遺失——只是得用慢方法去查,也就是去讀分頁表本身。這個查找叫做分頁表走訪,而在一次 TLB 失誤時,中央處理器的硬體走訪器會在原本那次記憶體存取能完成之前自動執行它。這就是 TLB 失誤的代價,而理解它就能解釋為何 TLB 失誤會比一般的快取失誤痛上許多。
分頁表不是一張扁平的大表——那會大到不像話。它是一棵多層的樹。在 4 KiB 頁的 x86-64 上有四層(從 page-map-level-4 一路往下經過 page directory 到 page table),所以轉譯一個虛擬位址意味著要跟隨四個指標,每一個都從記憶體讀出:讀第 4 層以找到第 3 層、讀第 3 層以找到第 2 層,依此類推,直到最後一個項目給出實體頁框。那是「光為了轉譯一個位址」就最多四次相依的記憶體存取,之後你才能去碰你原本想要的資料。這些存取本身每一次都可能在資料快取裡命中或失誤,所以最壞情況的一次走訪是好幾趟 DRAM 來回。轉譯成功後便會被插入 TLB,讓下一次對該頁的存取變快。
這正是 TLB 失誤陰險的原因:一次普通的快取失誤代價是去更低一層跑一趟,但一次 TLB 失誤的代價可能是「穿過最多四層、且每層都可被快取的指標追逐式走訪」,之後才是真正的資料存取。觸碰巨大位址範圍、頁區域性又差的程式,會把實實在在的時間花在分頁表走訪上。解藥與對抗 TLB 壓力的通則相同——改善區域性讓你重用一小組頁,或使用大頁讓每個 TLB 項目(與每次走訪)涵蓋遠多的記憶體,且每映射一個位元組所需的樹也更淺。
在一次 TLB 失誤時,單單一個 *p 的載入可能變成:讀 PML4 項目、讀 PDPT 項目、讀 PD 項目、讀 PT 項目,然後才讀 *p 處的真正資料——原始碼上看是一次,實際是五次記憶體存取。像 dtlb_load_misses.walk_active 這類效能計數器能揭露花在走訪上的時間。
原始碼上一次載入,實際最多五次記憶體存取——一次未被快取的轉譯所隱藏的代價。
在 x86/ARM 上走訪是由硬體完成、而非由作業系統陷阱觸發,所以單純的 TLB 失誤不會喚起核心——別把它和分頁錯誤(page fault)搞混,後者是另一個、代價高得多的事件,指該頁根本不在實體記憶體裡。