效能工程

分支、未命中與原子操作的成本模型

如果有人問「這一行要花多久?」,誠實的答案是「看情況」——但一位實作型工程師心裡帶著一張粗略的表,記著常見操作的「成本」,用唯一要緊的貨幣:每個操作大致佔住多少 CPU 週期或奈秒。那張心裡的表就是成本模型,而即使只有一個數量級版本的它,也能讓你在量測之前就預測,一段程式碼裡是哪些操作在主宰。

以下是值得記住的概略數字(數量級,在典型的現代伺服器 CPU 上;把它們當成「誤差幾倍以內」,而非精確值)。一條簡單算術指令(加法、比較)有效成本遠低於一個週期,因為 CPU 每週期重疊好幾條。一個正確預測的分支近乎免費;一個「預測錯」的分支大約花 10 到 20 個週期,因為 CPU 必須丟掉它沿著錯誤路徑開始的推測工作。讀取位於 L1 快取裡的資料花幾個週期;L2 約十幾個;L3 幾十個;但一次完整的快取「未命中」到主記憶體要花約 100 到 300 個週期——這是日常最大的一道懸崖,也是區域性主宰一切的原因。一次間接呼叫或虛擬分派(透過函式指標呼叫)加上一次可能的分支預測失敗加一次額外載入的成本,所以可預測時便宜、不可預測時是停滯。一次「原子」操作或一個沒被爭用的鎖大約花幾十個週期(它強制一個順序與對一條快取線的讀改寫);在爭用下,多個核心爭奪同一條線時,它可能花數百到數千個週期,因為線在乒乓。給個比例感:一次記憶體未命中大約是一次 L1 命中的一百倍,而一次跨核心被爭用的原子操作可以是沒被爭用版本的數百倍。

它之所以重要,是因為它把「先量測別猜測」變成「量測,但帶著有根據的預期」:你可以看著一個迴圈就預測那個隨機追指標(快取未命中)會主宰算術、或內層迴圈裡那個不可預測的分支才是真正的成本,然後用剖析器確認。關鍵的誠實:這些數字是近似值,在不同 CPU、世代與條件下會差好幾倍,而且它們描述的是「孤立的」操作——真實程式碼會重疊並隱藏掉大部分這種延遲,所以你不能單純把成本模型的總和加起來就相信它。用模型來形成假設、替嫌疑犯排序;用量測來決定。永遠別把這些當成精確值引用,並永遠在你真正出貨的硬體上重新量測。

概略(數量級,現代 CPU): 加法/預測中的分支 :< 1 週期 預測錯的分支 :約 10-20 週期 L1 命中/L2/L3 :約 4 / 12 / 40 週期 主記憶體快取未命中 :約 100-300 週期 未爭用的原子/CAS :約數十個週期 被爭用的原子(乒乓) :約數百到數千

一張數量級成本表:記憶體未命中與被爭用的原子是主宰的懸崖;算術近乎免費。

這些是近似值而非常數:它們因 CPU 與條件而差好幾倍,且真實程式碼會重疊操作,所以你不能直接相加。用模型替嫌疑犯排序,再在你實際的硬體上量測來決定。

又稱
rough cost tablelatency numbers every programmer should knowballpark cycle costs粗略成本表每位程式設計師該知道的延遲數字