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