每 CPU 資料(per-CPU data)
鎖是讓多個 CPU 安全共享資料的常見方式,但鎖有代價:核心為它爭奪,而持有共享資料的快取列在核心之間彈來彈去,這很慢。每 CPU 資料是一個巧妙的辦法,對某些資料完全避開這整個問題:不用一個所有核心爭搶的共享變數,而是給每一個核心它自己的私有副本。一個核心永遠只碰它自己的副本,所以沒有東西可爭搶,且在常見情況下根本不需要鎖。
想像一個核心在每個網路封包上都更新的統計計數器。作為一個共享計數器,每個核心遞增它都會在同一把鎖上爭奪、並讓同一條快取列彈跳——在繁忙的機器上是個嚴重的瓶頸。作為每 CPU 資料,每個核心無須協調地遞增它自己的本地計數器;核心永不碰彼此的副本,所以沒有爭奪、沒有快取列彈跳。核心安排成同一個變數名稱,依目前哪個 CPU 在跑而解析到不同的實體位置。當某段程式碼真的需要總和時,它走過所有每 CPU 副本並把它們相加——讀取是偶爾的、負擔得起,而熱門路徑(頻繁的更新)則保持快速且無鎖。
為何它重要:每 CPU 資料是核心可擴展性的基石,把一個被爭奪的共享資源變成許多無爭奪的本地資源。但它帶著一個你必須尊重的微妙要求:當一個執行緒正在存取它的每 CPU 變數時,它不可被遷移到不同的 CPU,也不可被以一種讓同 CPU 上另一個工作踐踏那份資料的方式搶占——所以每 CPU 存取在期間會搭配停用搶占(或使用正確的輔助函式)。它也用記憶體換速度(N 份副本而非一份),並使讀取全域總和更昂貴(你得把 N 份副本相加),這是消除寫入繁重的熱門路徑上爭奪的誠實代價。
共享計數器:每個核心做 lock; counter++; unlock——爭奪 + 快取列彈跳。每 CPU:每個核心無鎖地做 自己的計數器++;要讀總和,把所有核心的副本相加。存取期間釘在目前 CPU 上(停用搶占)。
給每個核心它自己的副本,使更新不需鎖也不需快取列彈跳;只在需要總和時把副本相加。
每 CPU 資料只有在工作於存取途中不會移到另一個 CPU 時才安全,所以它在停用搶占下使用(或用正確的 get_cpu_var/this_cpu 輔助函式)。它用記憶體(每 CPU 一份副本)與較昂貴的全域讀取(把所有副本相加)換取無爭奪的寫入路徑——別假設讀取很便宜。