效能工程

微基準測試的陷阱

微基準測試是一支小程式,孤立地對一件小事計時——譬如你的雜湊函式一次呼叫要多久。聽起來萬無一失:把呼叫放進迴圈、對迴圈計時、再相除。實務上,微基準是整個程式設計裡最容易做錯的事情之一,而一個錯的基準比沒有更糟,因為它用一個看起來很有信心的數字在說謊。

以下是經典陷阱,各附對策。第一,死碼消除(dead-code elimination):如果你算出一個結果卻從不使用,最佳化器有權刪掉整段運算,你最後是在對一個空迴圈計時,回報出一個快得不可能的時間。對策是消費那個結果——存進 volatile、回傳它、或用基準函式庫提供的「別最佳化掉」輔助函式。第二,常數摺疊與外提:如果你的輸入是編譯期常數,編譯器可能在建構時就把答案算好一次,或把不變的運算外提到迴圈外,於是你什麼都沒量到。要用編譯器無法預測的輸入。第三,暖機效應:最初幾次迭代要付出冷快取以及(在 JIT 語言中)編譯的成本,所以從第一次執行就計時會把啟動與穩態混在一起——先跑暖機階段,再量測。第四,量測負擔:如果你計時的東西只有幾奈秒,讀時鐘那個呼叫本身就可能花掉一樣多的時間,所以要對一批多次迭代計時再相除,而不是逐次計時。第五,缺乏統計嚴謹:一次執行只是雜訊;要在多次執行上回報分布(中位數與一個高百分位),而非單一數字。

它之所以重要,是因為這些陷阱動不動就產生差了 10 倍、100 倍、甚至無限倍(被刪掉的迴圈)的結果,而且無聲無息地失敗。還有兩個更深的誠實警告:微基準是在人工環境裡量一個函式——完美溫熱的快取、完美的分支預測、沒有周邊工作——所以一個在微基準中勝出的常式,可能在真實程式裡輸掉,那裡快取是冷的、別的程式碼在競爭;在相信之前,永遠拿微基準的勝利去對照整支程式的量測。

// 錯:結果沒用到 -> 最佳化器刪掉迴圈,回報約 0 奈秒 for (int i = 0; i < N; i++) hash(data[i]); // 對:強迫結果被觀察到 volatile uint64_t sink = 0; for (int i = 0; i < N; i++) sink ^= hash(data[i]);

死碼消除陷阱與其修法:把結果累加進一個 volatile 接收槽,讓最佳化器無法證明這工作是無用的。

volatile 能防止編譯器刪掉這份工作,但它不是基準測試的萬靈丹:它也可能擋掉正當的最佳化,因而灌水所量到的成本——若有,請優先用函式庫專門打造的「別最佳化」屏障(例如 benchmark::DoNotOptimize)。

又稱
benchmark trapsdead-code elimination of the benchmark微基準陷阱結果死碼消除