問題出在記憶體延遲,笨蛋:Pivot 實測+HN 討論整理

資料庫 CPU 顯示 100% 不代表真的在幹活,一半時間都在等記憶體。Pivot 官方實測+HN 七十餘則討論整理。

原文為 Pivot 官方部落格文章 It’s the memory latency, stupid(原文連結),HN 主串(討論連結,分數 221、留言 72)由 ntur1337 發起。本文為全文繁中翻譯+網友討論整理。

一句話結論

資料庫 CPU 顯示 100% 不代表真的在幹活——一半時間都在等記憶體;把聚合用的雜湊表切小塞進 CPU 快取,多做一點運算反而更快。

全文翻譯

當你看到資料庫 CPU 跑到 100%,它實際上在做什麼?為了方便,我們總假裝 CPU 是線性執行程式碼,但真相是 CPU 複雜得多:它可能同時執行多道指令,也可能一道都沒執行(儘管監控顯示 100%)。因為 CPU 能做的事這麼多,同樣顯示 100% 的兩顆 CPU,實際工作量可能天差地遠。CPU 只有在沒有任何任務需要它時才算閒置——這不代表 CPU 本體被充分利用了。那麼,為什麼 CPU 看起來 100%,實際卻沒跑滿?

答案通常是記憶體。

記憶體存取比 CPU 內部運算慢幾個數量級。具體一點:現代 CPU 每奈秒大約能處理四到八道指令;一次 RAM 存取卻要五十到一百奈秒。換算下來,一次 RAM 存取的時間,CPU 足夠執行約四百到八百道指令。

層級 延遲 容量
CPU 核心 — 每奈秒四至八道指令
L1 快取 約一奈秒 約 64KB
L2 快取 約三至四奈秒 約 1MB
L3 快取 約十至十五奈秒 最大數百 MB(高階處理器)
DRAM(記憶體) 約五十至一百奈秒 數 GB

因此 CPU 從設計之初就在繞開這個問題,最直接的手段是 CPU 快取。快取把資料放在比 RAM 更靠近 CPU 的地方,直接做進晶片裡。最近的 L1 快取每次存取約一奈秒,比 RAM 快五十到一百倍。「問題」是快取很小:現代 CPU 的 L1 大約 64KB,再遠的快取在高階處理器上最多也就數百 MB。好的資料庫會把資料切成小塊操作,充分利用快取;效能工程最關心的就是快取利用率。

用 GROUP BY 理解快取利用率

GROUP BY 是把資料集按某欄分組、再對另一欄做聚合(例如計數或加總)。「天真」的做法:開一張雜湊表,以分組鍵當鍵,越長越大;每來一列就查鍵在不在,在就更新對應的聚合結果。問題是:分組有數百萬種時,快取完全派不上用場,幾乎每次查表都是打到 RAM 的「冷」存取。CPU 看起來 100%,實際上一直在等冷記憶體。

作者把自家 Pivot 引擎「搞爛」做示範:拿掉複雜邏輯、換成最簡單的寫法,對 ClickBench 的 hits 資料集(九千九百萬列)跑 SELECT count(*), UserID FROM hits GROUP BY UserID——爆雷一下:約有一千七百萬個不重複 UserID。測試機是 CherryServers 裸機(AMD EPYC 9124,十六核心、128GB DDR5),只綁單核跑。

第一次跑三點九秒,第二次(資料已全在 page cache、無磁碟因素)三點六秒。perf stat 顯示:50.1% 的時間 CPU 因為等記憶體什麼都不能做,只有約一成六時間在真正執行運算。DRAM 載入共七千六百萬次,每次一個 cache line(六十四位元組),相乘就是這次查詢從 DRAM 拉了四點五 GB。

改良版:基數分割

解法分兩階段。第一階段把鍵切成四千零九十六個分區(取雜湊值前十二位元),每列按雜湊高位丟進對應分區暫存。第二階段逐分區建雜湊表聚合、再輸出,沿用第一階段算好的雜湊值。

乍看很怪:明明做了更多運算——每列兩種寫法都要雜湊加查表,這版還多了塞暫存分區的工。關鍵在聚合用的雜湊表變小了:切成四千零九十六份後,每個工作雜湊表更容易塞進 CPU 快取,查表命中快取的機率大增,省下的時間遠超多花的指令。實測第一次三點三秒、第二次二點七秒。perf 數據完全變了樣:因記憶體停滯從 50% 降到 23.5%,真正執行從 16.1% 升到 32.2%。注意這版實際上做了更多工(retired ops 從一百一十四億變成一百九十四億),但划算:把冷 DRAM 存取換成 CPU 指令,幾乎永遠是賺的。DRAM 拉取從四點五 GB 降到一點五 GB。

作者預告快取利用只是手段之一,還有預取、最大化記憶體頻寬等手法,下篇寫 JOIN probing 再談。不劇透太多,直接給 Pivot 正式版在這查詢單核的成績:一點八四八秒,DRAM 拉取九百五十四 MB;十六核全開只要一百七十二毫秒。

HN 網友討論整理

抓取時間為 2026 年 11 月 1 日,統計列以當時 live 值為準(分數 221、留言數 72,含已刪留言;頂層九則全讀,高回覆子串逐層追四到六層)。

機械同理心是失傳的技藝

boricj 在另一組看到大量訊息傳遞、深層巢狀虛擬方法、大型 DOM、現成 STL 容器,跑在快取少得可憐的 Cortex-A7 上;自己組用 2000 年代遊戲的 index 加世代號物件池、inline 配置容器,跑在 RAM 不到一 MB 的微控制器上,還得拿跑分證明自己不是瓶頸。jordand 補充:遊戲業老系統程式員很強但年事漸高,年輕一輩只想套現成引擎。Emjayen 更直接:千禧年泡沫時入行門檻被踩到地板,現在多數程式員連電腦怎麼工作都不懂,先懂這個才有資格談為了抽象犧牲什麼。

「過早最佳化」被濫用成擋箭牌

bunderbunder 拿著跑分證明效能差異又大又跟業務相關,code review 照樣以過早最佳化打回——那年代 Clean Code 正紅,很多人把審美標準當成客觀真理。hinkley 的對策是把最佳化包裝成可讀性改善,或把慢的部分抽成單一職責函式另開 commit 做,留著可 revert 大家就安心。swiftcoder 親身案例:團隊堅稱 CPU 已 100% 不可能再快,結果把記憶體存取模式清乾淨,requests-per-second 翻三倍。

快取友善對上乾淨程式碼

boricj 認為物件用指標互指是「乾淨」,但刪除即懸空指標地獄;table 加世代號寫法一樣整潔,還是從維護性和記憶體碎片角度做的重構,可惜學校只教七十年代的古典資料結構。jacquesm 花三週把一支程式縮到能整個塞進 L1,效能只能用驚人形容。brcmthrowaway 問鏈結串列的快取友善版長什麼樣,回覆指向展開鏈結串列與 B 樹家族。

新穎性質疑與回應

quotemstr 質疑這不就是基數分割聚合,老技術了,DuckDB、ClickHouse 都在了為什麼還要 Pivot。fluffyllemon 與 northes 回應:作者本來就沒宣稱新發明,重點是示範:快取友善的資料結構,花很小的力氣就能帶來多大改變。

延伸閱讀

brcmthrowaway 貼出 FreeBSD 經典長文〈每個程式員都該知道的記憶體知識〉,問有無 2026 重寫版;Matumio 回與其重寫不如重讀,CPU 架構沒變那麼多,變的是沒人有耐心讀長文。bugake 感嘆現代 CPU 有數百 MB 快取,比九十年代 Windows 整顆硬碟需求還大,電腦還慢,一半等 RAM,一半是幾十年抽象優先的低效軟體。bitwize 樂觀喊 LLM 能便宜地讓軟體重新變快,bunderbunder 苦笑自己還在跟 Cursor 纏鬥,阻止它超人類速度塞 N+1 查詢。

一頁懶人包

手機版可直接開備援連結:

https://static.blog.derekhsu.net/memory-latency-hn.html

譯註(非原文)

  • 標題化用自詹姆斯·卡維爾(James Carville,柯林頓競選操盤手)的名言,原文是談經濟,這裡借來談記憶體。
  • Retire 指 CPU 指令走完管線正式提交結果;backend_bound_memory 是 Top-down 分析中後端因記憶體停滯的槽位比例。
  • 第二次執行較快是因為資料已在作業系統 page cache,與查詢最佳化無關;perf 對比皆以第二次(無磁碟因素)為準。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *