Go 1.24 已不再建議使用 testing.b.N 開發(fā)性能測試用例
Go 開發(fā)者在使用 testing包編寫基準測試用例時,如果不注意,可能會遇到各種陷阱。這些陷阱,導致基準測試結果不準確。Go1.24 版本引入了一種新的基準測試編寫方式,它同樣易用,并且可以幫助規(guī)避編寫基準測試時的一些坑。
Go 1.24 版本推薦使用 testing.B.Loop代替 testing.B.N來編寫基準測試用例。
Go1.24 版本前,我們使用 b.N 來編寫基準測試用例,例如:
func Benchmark(b *testing.B) {
for range b.N {
... 要測量的代碼 ...
}
}
改用b.Loop僅需要微不足道的改動:
func Benchmark(b *testing.B) {
for b.Loop() {
... 要測量的代碼 ...
}
}
testing.B.Loop有很多優(yōu)點:
- 可以防止基準測試循環(huán)內的不當編譯優(yōu)化;
- 可以自動將設置和清理部分代碼耗時從基準測試時間統(tǒng)計中剔除;
- 代碼不應意外地依賴于總迭代次數(shù)或當前迭代。
上述這些優(yōu)點都是在使用 b.N編寫基準測試代碼時易犯的錯誤,這些錯誤會導致基準測試不準確。除了上述優(yōu)點之外,b.Loop風格的基準測試,還能在更短的時間執(zhí)行完。
接下來,我們來看下testing.B.Loop的優(yōu)勢以及如何有效地使用它。
舊基準測試循環(huán)問題
在 Go 1.24 之前,大部分的基準測試用例結構簡單。但是復雜的測試用例,卻需要很小心的編寫:
func Benchmark(b *testing.B) {
... setup ...
b.ResetTimer() // 如果設置可能很昂貴
for range b.N {
... 代碼測量 ...
... 使用匯點或累積防止未用代碼消除 ...
}
b.StopTimer() // 如果清理或報告可能很昂貴
... 清理 ...
... 報告 ...
}
如果設置 (setup)或清理 (cleanup)邏輯復雜,耗時較久,為了避免這些準備性的邏輯參與到核心代碼的耗時統(tǒng)計中,需要使用ResetTimer 和 StopTimer 方法,將這些時間剔除掉。但是真正開發(fā)的過程中, 可能有一些開發(fā)者會遺漏這些邏輯。即使開發(fā)者沒有遺漏,也很難判斷設置或清理過程是否“足夠耗時”到需要使用它們。
還有一個更微妙的陷阱,需要更深入的理解(示例源代碼):
func isCond(b byte) bool {
if b%3 == 1 && b%7 == 2 && b%17 == 11 && b%31 == 9 {
return true
}
return false
}
func BenchmarkIsCondWrong(b *testing.B) {
for range b.N {
isCond(201)
}
}
在這個例子中,用戶可能會觀察到 isCond 在亞納秒級別的時間內執(zhí)行。CPU 的速度很快,但并沒有快到這個程度!這個看似異常的結果源于 isCond 被內聯(lián)處理,并且由于其結果從未被使用,編譯器將其視為無效代碼而進行消除。
因此,這個基準測試根本沒有測量 isCond。它測量的是進行無操作所需的時間。在這種情況下,亞納秒的結果是一個明顯的警示,但在更復雜的基準測試中,部分無效代碼消除可能導致看起來合理但實際上并未測量預期內容的結果。
testing.B.Loop可以帶來哪些好處?
與 b.N 風格的基準測試不同,testing.B.Loop 能夠跟蹤其首次調用時間以及基準測試的最終迭代結束時刻。循環(huán)開始時的 b.ResetTimer 和結束時的 b.StopTimer 被整合進 testing.B.Loop,消除了手動管理基準測試計時器以進行初始化和清理代碼的開發(fā)步驟。
另外,Go 編譯器現(xiàn)在可以探測到只調用 testing.B.Loop 時的循環(huán),并阻止 testing.B.Loop 內的代碼死循環(huán)。
testing.B.Loop 的另一個優(yōu)點是其一次性快速提升的方法。對于 b.N 風格的基準測試,測試包必須多次調用基準測試函數(shù),并使用不同的 b.N 值逐步增加,直到測量時間達到一個閾值。相比之下,b.Loop 可以簡單地運行基準測試循環(huán),直到達到時間閾值,只需調用基準測試函數(shù)一次。
b.N 風格循環(huán)的某些限制仍適用于 b.Loop 風格的循環(huán)。用戶仍需在必要時負責在基準測試循環(huán)中管理計時器。(示例源)
func BenchmarkSortInts(b *testing.B) {
ints := make([]int, N)
for b.Loop() {
b.StopTimer()
fillRandomInts(ints)
b.StartTimer()
slices.Sort(ints)
}
}
在這個例子中,為了測試 slices.Sort 方法的就地排序性能,每次迭代都需要一個隨機初始化的數(shù)組。開發(fā)菏澤仍需在這些情況下手動管理計時器。
此外,基準測試函數(shù)體中仍需要只有一個這樣的循環(huán)(b.N風格循環(huán)不能與b.Loop風格循環(huán)共存),并且循環(huán)的每次迭代應該做相同的事情。
何時使用
testing.B.Loop 方法,是從 Go 1.24 版本起,編寫基準測試的首選方式。使用示例如下:
func Benchmark(b *testing.B) {
... 設置 ...
for b.Loop() {
// 可選的循環(huán)內部設置/清理計時器控制
... 要測量的代碼 ...
}
... 清理 ...
}