閱讀筆記 / READING NOTE 這篇整理 Jeff Dean 與 Sanjay Ghemawat 的《Performance Hints》精華。原文:abseil.io/fast/hints.html(2023 年初版,2025 更新;聚焦單一程式的效能調校,不含分散式系統與 ML 硬體)。插圖由 Gemini 生成。 ← Learn
JEFF DEAN 的效能筆記 · ABSEIL PERFORMANCE HINTS

如果 1 奈秒是 1 秒,一個封包要飛 4.76 年

Google($GOOGL)有一份流傳二十年的效能心法,作者是兩位傳奇工程師:Jeff Dean 與 Sanjay Ghemawat。

MapReduce、Bigtable、Spanner——你聽過的 Google 基礎建設,幾乎都有這兩個名字。

文件不長,但全是他們親手調校二十多年的濃縮。

這一頁挑出最精華、最有趣的部分,一次看完。

示意插畫:工程師在信封背面動手估算,背後是晶片、記憶體、磁碟、網路依序漸遠的山景

一句話

效能不能等寫完再救——完全不管效能的系統,最後會慢得很「平均」,連可以搶救的熱點都沒有。

所以 Google 工程師動手前先估算。估算的本錢,是背下一張延遲數字表:從 L1 快取到跨洲封包,頭尾差 3 億倍。

有了這張表,畫設計圖時花五分鐘,就能刪掉延遲差 30 倍的錯誤方案。

這套習慣的戰果是真的:光是把一個預設值從 200 改成 10,整台伺服器就省下 7.5% 的 CPU。

01

為什麼不能「寫完再優化」

電腦科學家 Donald Knuth——《The Art of Computer Programming》的作者、圖靈獎得主——說過一句被引用到爛的話:「過早的最佳化是萬惡之源。」但幾乎沒有人引用完整版:

大約 97% 的情況,我們確實該忽略微小的效率——過早的最佳化是萬惡之源。但剩下關鍵的 3%,我們不該放過。 Knuth 還補過另一段:某個範例改寫後只快了 12%,很多人會說不痛不癢。他不同意——在成熟的工程領域裡,一個輕鬆到手的 12% 改善,從來不會被說成微不足道。

這份文件講的,就是那關鍵的 3%。兩位作者不贊成「先讓程式能動,效能以後再說」——因為以後通常來不及。一個從頭到尾都不考慮效能的大系統,慢不會集中在某個地方,而是整個系統每處都慢一點。等你拿 profile(效能剖析,量測程式時間都花在哪的報告)出來看,圖攤開一片平坦、沒有突出的熱點——想救,都不知道從哪裡下手。

示意插畫:兩艘船,一艘船身有一個大破洞,另一艘船身佈滿幾十個小破洞
船底破一個大洞,塞住就好。破八十個小洞——你只能一直舀水。
有熱點的 profile 少數地方吃掉大部分時間 優化這裡 平坦的 profile 效能均勻流失,沒有熱點 從哪裡開始?

左邊:一根柱子特別高,熱點清楚,知道往哪修。右邊:每根柱子都差不多,修哪裡都不痛不癢——這就是「寫完再優化」常見的下場。示意,非實測數值。

他們的建議 寫程式的當下,只要不犧牲可讀性,就順手挑比較快的寫法。就這樣。
02

一張該背下來的表

想在動手前判斷快慢,得先知道電腦裡每一種基本操作大概要花多少時間。這張表源自 Jeff Dean 2007 年在 Stanford 的演講(有一場內容相近的 2011 年錄影,和當年的投影片 PDF),2025 年更新——從 CPU 快取到跨洲網路,每一級都比上一級慢十倍以上。

表裡的時間全部以奈秒(ns)計。1 奈秒是十億分之一秒,短到完全超出人類的感覺——所以下圖右欄做了一個換算:假設 1 奈秒放大成 1 秒,每個操作會變成多久?L1 快取讀一次是 0.5 秒,差不多一次心跳;一個封包飛到荷蘭再飛回來,是 4.76 年。

延遲數字階梯 · 對數尺度:每格差 10 倍 人類時間(若 1ns = 1 秒) 1ns10ns100ns 1µs10µs100µs 1ms10ms100ms1s L1 快取讀取 L2 快取讀取 分支預測失敗 mutex 鎖/解鎖 主記憶體讀取 Snappy 壓縮 1KB SSD 讀 4KB 同機房內往返 記憶體循序讀 1MB 100Gbps 讀 1MB SSD 讀 1MB 硬碟尋軌 seek 硬碟循序讀 1MB 跨洲封包 CA→NL→CA 0.5 ns 3 ns 5 ns 15 ns 50 ns 1 µs 20 µs 50 µs 64 µs 100 µs 1 ms 5 ms 10 ms 150 ms 0.5 秒(一次心跳) 3 秒 5 秒 15 秒 50 秒 ≈17 分鐘 ≈5.5 小時 ≈14 小時 ≈18 小時 ≈28 小時 ≈11.6 天 ≈58 天 ≈116 天 ≈4.76 年 CPU 記憶體 儲存 網路 Snappy 壓縮實際是 CPU 運算,只是數值量級接近網路類操作 jazzlien.com/learn · Dean & Ghemawat, Performance Hints (2025) · 2026.08

14 個操作、橫跨 9 個數量級。右欄是「若 1ns = 1 秒」的換算——L1 快取是一次心跳,硬碟循序讀 1MB 是 116 天,跨洲封包來回是 4.76 年。

原始表格(ns,可直接複製使用):

L1 cache reference                             0.5 ns
L2 cache reference                             3 ns
Branch mispredict                              5 ns
Mutex lock/unlock (uncontended)               15 ns
Main memory reference                         50 ns
Compress 1K bytes with Snappy              1,000 ns
Read 4KB from SSD                         20,000 ns
Round trip within same datacenter         50,000 ns
Read 1MB sequentially from memory         64,000 ns
Read 1MB over 100 Gbps network           100,000 ns
Read 1MB from SSD                      1,000,000 ns
Disk seek                              5,000,000 ns
Read 1MB sequentially from disk       10,000,000 ns
Send packet CA->Netherlands->CA      150,000,000 ns

資料來源:Dean & Ghemawat, Performance Hints(abseil.io/fast/hints.html),更新自 2007 年 Stanford 演講的表格。

這張表的頭尾,差了 3 億倍。 同樣讀 1MB:記憶體 64 微秒、傳統硬碟 10 毫秒,差 156 倍。背表不是為了精確——是為了數量級的直覺:知道什麼快、什麼慢、差幾個零,才有能力估算。原文也建議把你自己系統的常用操作補進表裡:一次資料庫查詢要多久、一次雲端 API 呼叫要多久。
03

動手前,先抓個數量級

有了這張表,估算只有三步:數一數要做幾次操作、乘上每次要花的時間、加總。原文叫它 back-of-envelope calculation——直譯「信封背面計算」,意思是隨手一張紙就算得完。

拿文件裡的例子練一次。你要做一頁顯示 30 張縮圖的網頁,原始圖檔每張約 1MB,放在磁碟上。三種設計:

一顆磁碟依序讀——每張圖 5ms 尋軌(磁頭移到資料所在位置)+ 10ms 傳輸,30 張讀完要 450ms。

圖散在幾百顆磁碟上,平行讀——硬體做的總工作量一樣,但延遲除以磁碟數,大約 15ms。

全部搬進一顆 SSD——每張 20µs + 1ms,30 張大約 30ms。

一頁 30 張縮圖:三種設計的延遲(真實比例) ① 循序讀磁碟 450 ms (5ms seek + 10ms 傳輸) × 30 張 ② 平行讀 K 台磁碟 ~15 ms 資源成本不變,延遲 ÷K(數百顆碟的分散式檔案系統) ③ 全部放單顆 SSD ~30 ms (20µs 尋址 + 1ms 傳輸) × 30 張 jazzlien.com/learn · Dean & Ghemawat, Performance Hints (2025) · 2026.08

同一款 time bar、同一個起點,依真實毫秒數繪製——① 的長條長得誇張,② ③ 幾乎是細線,這正是「延遲差 30 倍」的視覺證據。

同樣的資源成本,延遲差 30 倍——這個結論,畫設計圖時算五分鐘就知道。 估算最大的價值不是算得準,是在動工之前,就把不用做的方案刪掉。
04

從 Google 的真實改動,偷學六件事

這份文件最珍貴的地方,是附上幾十個 Google 內部真實的程式碼改動與 benchmark 數字。挑六個最有畫面的:

把 200 改成 10,省下 7.5% CPU

改動前200 個節點 改動後10 個節點

Google 的網頁搜尋伺服器每收到一個查詢,會先預留一塊能放 200 個「查詢解析節點」的記憶體,準備拆解你輸入的關鍵字。但一般查詢就兩三個詞,200 個根本用不到——等於每個查詢都先付一筆用不到的開銷。把預設值改成 10,整台伺服器的 CPU 用量少了 7.5%。

教訓:預設值也是程式碼,也該被質疑。

換一個演算法,快 50 倍

改動前22 µs 改動後0.5 µs

mutex(互斥鎖)的死結偵測,原本的演算法在 2,000 顆鎖就會撞牆。換成 2006 年一篇論文的動態拓撲排序後,單次操作從 22µs 降到 0.5µs,鎖的數量上限直接解除,還順便揪出一批潛伏已久的真死結。

教訓:真正的大勝負在演算法層,不在微調。

一把鎖切成 64 把,時間省 69%

改動前11.9 秒 改動後3.7 秒

Spanner 追蹤交易的資料結構,原本所有執行緒搶同一把鎖。切成 64 個 shard(分片)、各自上鎖之後,benchmark 總時間少了 69%。

教訓:「CPU 看起來很閒」的真兇,常常是大家在排隊等同一把鎖。

疫情爆量,Meet 靠改抽樣撐住

改動前224 ns 改動後85 ns

Google Meet 對每個封包都做效能量測。COVID 爆發、流量暴衝時,他們把最貴的量測從每 10 包抽 1 包降到每 32 包抽 1 包——32 是 2 的次方,抽樣判斷更便宜——每包的量測成本從 224ns 降到 85ns。

教訓:監控數據本身也有價格,撐不住的時候先砍它。

Protobuf 換成普通 struct,快 20 倍

改動前17.4 µs 改動後0.8 µs

存 1,000 個座標點、再把 Y 座標加總:用 protobuf 要 17.4µs,換成普通的 struct 陣列只要 0.8µs——差 20 倍。

教訓:資料不需要序列化傳輸,就別住在 protobuf 裡。

2001 年,把索引搬進記憶體

改動前6.7 ms/查詢 改動後2.0 ms/查詢

Google 把搜尋索引從磁碟搬進記憶體那一役,做了一整輪這種等級的優化——解碼加速、砍掉邊界檢查、按需解碼——單機查詢輸送量從 150 QPS 拉到 500 以上,3.3 倍,換算下來每個查詢從 6.7ms 壓到 2ms。

教訓:一次大改版,就是幾十個小技巧的加總。

05

接手一片平坦的系統怎麼辦

真的接手了一片平坦、沒有熱點的系統呢?文件給了六條路。核心精神都一樣:熱點不明顯的時候,改用「累積」和「結構」的方式贏。

別小看二十個 1%

在同一個子系統裡做二十次各 1% 的改善,是完全做得到的事——加總起來,就是別人眼中的一次大改版。前提是要有穩定的 microbenchmark(微基準測試,對一小段程式碼反覆計時),才能驗證每個 1% 是真的發生了。

文件裡的實例:XLA(Google 的機器學習編譯器)靠六個這種等級的小修改——雜湊表查兩次改成查一次、省掉一次多餘的複製、把一個檢查換成便宜的版本——編譯時間快了約 15%。第 4 節那個 2001 年的索引改版,也是同一種打法。

往 call stack 上層找迴圈

用 flame graph(火焰圖)找靠近頂端的迴圈。文件裡的真實例子:本來逐節點、逐邊慢慢把圖建起來,每加一條邊都要重新檢查一次;改成整張圖一次建好,每條邊的重複檢查全數消失。

揪出殺雞用牛刀的地方

很多慢,是因為用了太通用的工具。文件的例子:程式反覆跑正規表示式,但實際上只需要比對字串開頭——換成單純的 prefix match,馬上快。

對記憶體配置下手

拿一份 allocation profile,從配置次數最多的地方改起。省的是兩層:配置動作本身的時間,以及每次配置都落在不同 cache line(快取行,快取搬資料的最小單位)造成的 cache miss。

讓硬體數據說話

收集 performance counter(硬體效能計數器)做的 profile,讓它直接點名 cache miss 率特別高的函式——這是一般計時器看不到的層次。

退一步,改結構

微調救不了的,回到演算法與架構層重想。上一節那個快 50 倍的死結偵測,走的就是這條路——不是把舊程式碼磨快,是整個換掉。

火焰圖長這樣(示意) 每往上一層 = 往呼叫堆疊裡走一層 · 方塊橫寬 = 吃掉的時間佔比 hash_lookup() alloc() 其他 check_cycles() add_edge() render() build_graph() 其他工作 main() 又高又寬的方塊——先看它

示意用的假想火焰圖。最底層是程式進入點,每往上一層就是往呼叫堆疊裡走一層;方塊的橫寬,是那個函式(連同它呼叫的所有東西)吃掉的時間佔比。讀法是由上往下找「又高又寬」:像這裡的 check_cycles(),離頂端很近、又佔掉一半以上的寬度——正是「每加一條邊就重新檢查一次」那種迴圈在火焰圖上現形的樣子。示意,非實測數值。

工具與陷阱 pprof 看概況、perf 看細節、microbenchmark 驗證改善。一個反直覺的陷阱:CPU 使用率很低,不代表系統很閒——很多時候是所有執行緒都卡在等同一把鎖。
示意插畫:二十片薄片堆疊成一塊完整的磚塊高度
二十片 1% 疊起來,就是別人眼中不可能的 20%。
06

出處與延伸閱讀

出處與作者

Jeffrey Dean、Sanjay Ghemawat,《Performance Hints》,2025(abseil.io/fast/hints.html)。

Jeff Dean 現任 Google 首席科學家;Sanjay Ghemawat 是 Google Senior Fellow。兩人搭檔寫出 MapReduce、Bigtable、Spanner 等定義了現代分散式系統的作品。

延伸閱讀

同站的 Abseil「Performance Tips of the Week」系列,一週一篇的效能短文。

原文後半還有大量逐行的 C++ 改動範例——資料結構、記憶體配置、並行與 protobuf——胃口被吊起來的話,值得直接讀原文。

07

名詞小抄

這篇的關鍵詞,一句話版。

L1/L2 快取 L1/L2 cache
CPU 內建的小容量高速記憶體,離運算單元越近越快,L1 讀一次只要 0.5ns。
分支預測失敗 branch mispredict
CPU 猜錯 if/else 要走哪條路,得把做到一半的運算作廢重來,一次約 5ns。
mutex 互斥鎖
同一時間只讓一個執行緒碰共用資料的鎖。無人競爭時鎖一次約 15ns。
Snappy
Google 開發的輕量壓縮演算法,重速度不重壓縮率,壓 1KB 約 1µs。
尋軌 disk seek
傳統硬碟把磁頭移到資料所在位置的動作,一次約 5ms——是表上最貴的本機操作。
profile 效能剖析
量測程式時間花在哪裡的報告;「一片平坦」指沒有明顯熱點可修。
back-of-envelope 信封背面估算
不寫程式、只靠「操作次數 × 每次耗時」的紙上估算,用來快速比較方案。
flame graph 火焰圖
把 profile 畫成一層層的橫條,越寬代表越花時間,用來找 call stack 裡值得改的熱點。
cache line 快取行
快取搬資料的最小單位(通常 64 bytes);資料散在越多條 cache line 上,cache miss 越多。
pprof
Google 開源的效能剖析工具,適合先看高層概況。
perf
Linux 內建的效能分析工具,能看到硬體層級的細節。
microbenchmark 微基準測試
對一小段程式碼反覆計時的測試,用來驗證小改善是不是真的發生。
lock contention 鎖競爭
多執行緒搶同一把鎖,大家都在等——CPU 使用率看起來很低,系統卻卡住。
QPS queries per second
每秒能處理的查詢數,衡量伺服器輸送量的單位。
返回 Learn,看更多圖文好讀版
原文 Performance Hints · Jeff Dean、Sanjay Ghemawat(abseil.io,2025 更新版)· 中文圖文整理:Jazz Lien · 插圖由 Gemini 生成