NUMA 感知配置
/ NOO-muh /
在一台大型多插槽伺服器上,記憶體不是一個每顆 CPU 都能等速存取的均勻池。想像一間橫跨好幾層樓的辦公室,每個團隊的檔案櫃就在自己那層。從你自己這層拿檔案很快;要拿另一層的就得爬樓梯。這就是 NUMA——Non-Uniform Memory Access(非一致記憶體存取):機器有好幾個記憶體節點,每個都緊鄰一群 CPU,而一顆 CPU 讀自己節點的記憶體比讀遠端節點快。NUMA 感知配置(NUMA-aware allocation)的意思是配置器試圖給每個執行緒來自它所執行 CPU 最近節點的記憶體。
它實際如何運作,取決於作業系統的一個細節:標準的本地節點(或首次接觸,first-touch)策略。當你呼叫 malloc() 拿到一個位址時,還沒有任何實體記憶體被提交——該頁要等到某顆 CPU 第一次接觸(寫入)它時才真正被指派到一個實體節點,而核心會把那一頁放在接觸該頁之 CPU 的本地節點上。所以實務規則是:第一個寫入某頁的執行緒,應該就是之後大多會使用它的執行緒。NUMA 感知配置器在此之上,藉由保有釘在本地節點的每節點 arena 或每執行緒快取,讓一個執行緒的配置來自、並首次接觸於它自己的節點。收益是實在的:一次遠端記憶體存取的延遲可能明顯更高、頻寬更低,所以一台不小心把執行緒資料散落到遠端節點的伺服器,可能在沒有明顯原因下慢上許多。
它在哪裡咬人:高核心數的資料庫與 HPC 工作負載,經典錯誤是一個執行緒配置並初始化一個大陣列(把每一頁都首次接觸到自己的節點上),然後其他節點上的許多執行緒遠端猛敲它。修法是平行地配置並初始化,讓每個執行緒首次接觸自己的那一片,或使用 NUMA 感知的配置器與 API(numactl、libnuma、mbind())來控制放置。誠實的提醒:NUMA 感知只在多插槽的 NUMA 硬體上才重要;在單插槽機器上它無關緊要,而搞錯(過度釘定)可能因把忙碌的節點餓死、而鄰居節點閒置而傷害效能。
# 把行程釘到節點 0 的 CPU「並」釘到節點 0 的記憶體: $ numactl --cpunodebind=0 --membind=0 ./db_server /* first-touch 規則:「第一個」寫入某頁的執行緒讓它落在本地。 */ /* 所以要平行初始化,而非全由一個執行緒做: */ #pragma omp parallel for for (size_t i = 0; i < n; i++) big[i] = 0; /* 每個執行緒接觸自己那片 */
在 first-touch 下,第一個寫入某頁的執行緒決定它的節點;平行初始化讓每個執行緒的資料保持本地。
malloc() 回傳一個位址並不放置實體記憶體;在 first-touch 下,頁會落在第一個寫入它的節點上。在一個執行緒配置、卻在另一個使用,是經典的 NUMA 效能陷阱。