Google($GOOGL)有一份流傳二十年的效能心法,作者是兩位傳奇工程師:Jeff Dean 與 Sanjay Ghemawat。
MapReduce、Bigtable、Spanner——你聽過的 Google 基礎建設,幾乎都有這兩個名字。
文件不長,但全是他們親手調校二十多年的濃縮。
這一頁挑出最精華、最有趣的部分,一次看完。
效能不能等寫完再救——完全不管效能的系統,最後會慢得很「平均」,連可以搶救的熱點都沒有。
所以 Google 工程師動手前先估算。估算的本錢,是背下一張延遲數字表:從 L1 快取到跨洲封包,頭尾差 3 億倍。
有了這張表,畫設計圖時花五分鐘,就能刪掉延遲差 30 倍的錯誤方案。
這套習慣的戰果是真的:光是把一個預設值從 200 改成 10,整台伺服器就省下 7.5% 的 CPU。
電腦科學家 Donald Knuth——《The Art of Computer Programming》的作者、圖靈獎得主——說過一句被引用到爛的話:「過早的最佳化是萬惡之源。」但幾乎沒有人引用完整版:
這份文件講的,就是那關鍵的 3%。兩位作者不贊成「先讓程式能動,效能以後再說」——因為以後通常來不及。一個從頭到尾都不考慮效能的大系統,慢不會集中在某個地方,而是整個系統每處都慢一點。等你拿 profile(效能剖析,量測程式時間都花在哪的報告)出來看,圖攤開一片平坦、沒有突出的熱點——想救,都不知道從哪裡下手。

左邊:一根柱子特別高,熱點清楚,知道往哪修。右邊:每根柱子都差不多,修哪裡都不痛不癢——這就是「寫完再優化」常見的下場。示意,非實測數值。
想在動手前判斷快慢,得先知道電腦裡每一種基本操作大概要花多少時間。這張表源自 Jeff Dean 2007 年在 Stanford 的演講(有一場內容相近的 2011 年錄影,和當年的投影片 PDF),2025 年更新——從 CPU 快取到跨洲網路,每一級都比上一級慢十倍以上。
表裡的時間全部以奈秒(ns)計。1 奈秒是十億分之一秒,短到完全超出人類的感覺——所以下圖右欄做了一個換算:假設 1 奈秒放大成 1 秒,每個操作會變成多久?L1 快取讀一次是 0.5 秒,差不多一次心跳;一個封包飛到荷蘭再飛回來,是 4.76 年。
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 演講的表格。
有了這張表,估算只有三步:數一數要做幾次操作、乘上每次要花的時間、加總。原文叫它 back-of-envelope calculation——直譯「信封背面計算」,意思是隨手一張紙就算得完。
拿文件裡的例子練一次。你要做一頁顯示 30 張縮圖的網頁,原始圖檔每張約 1MB,放在磁碟上。三種設計:
一顆磁碟依序讀——每張圖 5ms 尋軌(磁頭移到資料所在位置)+ 10ms 傳輸,30 張讀完要 450ms。
圖散在幾百顆磁碟上,平行讀——硬體做的總工作量一樣,但延遲除以磁碟數,大約 15ms。
全部搬進一顆 SSD——每張 20µs + 1ms,30 張大約 30ms。
同一款 time bar、同一個起點,依真實毫秒數繪製——① 的長條長得誇張,② ③ 幾乎是細線,這正是「延遲差 30 倍」的視覺證據。
這份文件最珍貴的地方,是附上幾十個 Google 內部真實的程式碼改動與 benchmark 數字。挑六個最有畫面的:
Google 的網頁搜尋伺服器每收到一個查詢,會先預留一塊能放 200 個「查詢解析節點」的記憶體,準備拆解你輸入的關鍵字。但一般查詢就兩三個詞,200 個根本用不到——等於每個查詢都先付一筆用不到的開銷。把預設值改成 10,整台伺服器的 CPU 用量少了 7.5%。
教訓:預設值也是程式碼,也該被質疑。
mutex(互斥鎖)的死結偵測,原本的演算法在 2,000 顆鎖就會撞牆。換成 2006 年一篇論文的動態拓撲排序後,單次操作從 22µs 降到 0.5µs,鎖的數量上限直接解除,還順便揪出一批潛伏已久的真死結。
教訓:真正的大勝負在演算法層,不在微調。
Spanner 追蹤交易的資料結構,原本所有執行緒搶同一把鎖。切成 64 個 shard(分片)、各自上鎖之後,benchmark 總時間少了 69%。
教訓:「CPU 看起來很閒」的真兇,常常是大家在排隊等同一把鎖。
Google Meet 對每個封包都做效能量測。COVID 爆發、流量暴衝時,他們把最貴的量測從每 10 包抽 1 包降到每 32 包抽 1 包——32 是 2 的次方,抽樣判斷更便宜——每包的量測成本從 224ns 降到 85ns。
教訓:監控數據本身也有價格,撐不住的時候先砍它。
存 1,000 個座標點、再把 Y 座標加總:用 protobuf 要 17.4µs,換成普通的 struct 陣列只要 0.8µs——差 20 倍。
教訓:資料不需要序列化傳輸,就別住在 protobuf 裡。
Google 把搜尋索引從磁碟搬進記憶體那一役,做了一整輪這種等級的優化——解碼加速、砍掉邊界檢查、按需解碼——單機查詢輸送量從 150 QPS 拉到 500 以上,3.3 倍,換算下來每個查詢從 6.7ms 壓到 2ms。
教訓:一次大改版,就是幾十個小技巧的加總。
真的接手了一片平坦、沒有熱點的系統呢?文件給了六條路。核心精神都一樣:熱點不明顯的時候,改用「累積」和「結構」的方式贏。
在同一個子系統裡做二十次各 1% 的改善,是完全做得到的事——加總起來,就是別人眼中的一次大改版。前提是要有穩定的 microbenchmark(微基準測試,對一小段程式碼反覆計時),才能驗證每個 1% 是真的發生了。
文件裡的實例:XLA(Google 的機器學習編譯器)靠六個這種等級的小修改——雜湊表查兩次改成查一次、省掉一次多餘的複製、把一個檢查換成便宜的版本——編譯時間快了約 15%。第 4 節那個 2001 年的索引改版,也是同一種打法。
用 flame graph(火焰圖)找靠近頂端的迴圈。文件裡的真實例子:本來逐節點、逐邊慢慢把圖建起來,每加一條邊都要重新檢查一次;改成整張圖一次建好,每條邊的重複檢查全數消失。
很多慢,是因為用了太通用的工具。文件的例子:程式反覆跑正規表示式,但實際上只需要比對字串開頭——換成單純的 prefix match,馬上快。
拿一份 allocation profile,從配置次數最多的地方改起。省的是兩層:配置動作本身的時間,以及每次配置都落在不同 cache line(快取行,快取搬資料的最小單位)造成的 cache miss。
收集 performance counter(硬體效能計數器)做的 profile,讓它直接點名 cache miss 率特別高的函式——這是一般計時器看不到的層次。
微調救不了的,回到演算法與架構層重想。上一節那個快 50 倍的死結偵測,走的就是這條路——不是把舊程式碼磨快,是整個換掉。
示意用的假想火焰圖。最底層是程式進入點,每往上一層就是往呼叫堆疊裡走一層;方塊的橫寬,是那個函式(連同它呼叫的所有東西)吃掉的時間佔比。讀法是由上往下找「又高又寬」:像這裡的 check_cycles(),離頂端很近、又佔掉一半以上的寬度——正是「每加一條邊就重新檢查一次」那種迴圈在火焰圖上現形的樣子。示意,非實測數值。

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——胃口被吊起來的話,值得直接讀原文。
這篇的關鍵詞,一句話版。