Go語言基準測試(benchmark)三部曲之三:提高篇

程式設計師欣宸發表於2023-11-03

歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套原始碼):https://github.com/zq2599/blog_demos

本篇概覽

-《Go語言基準測試(benchmark)三部曲》已近尾聲,經歷了《基礎篇》和《記憶體篇》的實戰演練,相信您已熟練掌握了基準測試的常規操作以及各種引數的用法,現在可以學習一些進階版的技能了,在面對複雜一些的場景也能高效完成基準測試,另外還有幾個坑也要提前瞭解,避免以後掉進去

ResetTimer

  • 有時候,在基準測試前會有些準備工作,這些準備工作的耗時會影響基準測試的結果,舉例如下,BenchmarkFib是常規的基準測試,而BenchmarkFibWithPrepare多了八百毫秒的準備時間
func BenchmarkFib(b *testing.B) {
	for n := 0; n < b.N; n++ {
		fib(30)
	}
}

// BenchmarkFibWithPrepare 進入正式測試前需要耗時做準備工作的case
func BenchmarkFibWithPrepare(b *testing.B) {
	// 假設這裡有個耗時800毫秒的初始化操作
	<-time.After(800 * time.Millisecond)

	// 這下面才是我們們真正想做基準測試的程式碼
	for n := 0; n < b.N; n++ {
		fib(30)
	}
}
  • 同時執行上述兩個基準測試,命令和結果如下,可見因為準備工作的耗時,BenchmarkFibWithPrepare方法的測試結果遠不及BenchmarkFib,這與事實是不符合的,因為BenchmarkFibWithPrepare方法的測試目標沒有變化,但是因為自身的準備工作導致測試結果出現較大偏差
go test -bench='BenchmarkFib|BenchmarkFibWithPrepare' benchmark-demo
goos: darwin
goarch: arm64
pkg: benchmark-demo
BenchmarkFib-8                       325           3637442 ns/op
BenchmarkFibWithPrepare-8             50          20173566 ns/op
PASS
ok      benchmark-demo  14.871s
  • 解決上述問題的思路是不要將準備工作的耗時算入基準測試,實現起來很簡簡,如下圖黃色箭頭所示,b.ResetTimer()重置了計時器,前面的耗時都與基準測試無關
    在這裡插入圖片描述
  • 再做一次基準測試,結果如下,可見800毫秒帶來的偏差已被去除
go test -bench='BenchmarkFib|BenchmarkFibWithPrepare' benchmark-demo
goos: darwin
goarch: arm64
pkg: benchmark-demo
BenchmarkFib-8                       325           3616239 ns/op
BenchmarkFibWithPrepare-8            316           3729323 ns/op
PASS
ok      benchmark-demo  5.628s

StopTimer & StartTimer

  • 前面透過ResetTimer消除了基準測試前的多餘耗時,但是如果多餘的耗時出現在基準測試過程中呢?程式碼如下所示,fib是本次測試的目標,如果每次fib結束後都要做一些耗時的清理工作(這裡用10毫秒延時來模仿),才能再次fib,那又該如何消除這10毫秒對基準測試的影響呢?
func BenchmarkFibWithClean(b *testing.B) {
	// 這下面才是我們們真正想做基準測試的程式碼
	for n := 0; n < b.N; n++ {
		fib(30)

		// 假設這裡有個耗時100毫秒的清理操作
		<-time.After(10 * time.Millisecond)
	}
}
  • 先來看看每次fib之後的10毫秒是否會影響基準測試,執行測試的命令和測試結果如下,可見,和沒有任何耗時的BenchmarkFib方法相比,BenchmarkFibWithClean的測試結果與fib的真實效能相去甚遠
go test -bench='BenchmarkFib$|BenchmarkFibWithClean' benchmark-demo
goos: darwin
goarch: arm64
pkg: benchmark-demo
BenchmarkFib-8                       322           3610100 ns/op
BenchmarkFibWithClean-8               81          16139196 ns/op
PASS
ok      benchmark-demo  3.002s
  • 對於這種每次呼叫fib之前或者之後都會出現的額外耗時操作,可以用b.StartTimer()b.StopTimer()的組合來消除掉,簡單的說就是StartTimer會開啟基準測試的計時,StopTimer會暫停計時,具體的使用方法如下
// BenchmarkFibWithClean 假設每次執行完fib方法後,都要做一次清理操作
func BenchmarkFibWithClean(b *testing.B) {
	// 這下面才是我們們真正想做基準測試的程式碼
	for n := 0; n < b.N; n++ {
		// 繼續記錄耗時
		b.StartTimer()

		fib(30)

		// 停止記錄耗時
		b.StopTimer()

		// 假設這裡有個耗時100毫秒的清理操作
		<-time.After(10 * time.Millisecond)
	}
}
  • 再次測試,結果如下,去除了多餘耗時的基準測試結果,從之前16139196ns恢復到7448678ns,然而,和原始的沒有任何處理的BenchmarkFib結果相比依然有一倍左右的差距,看來StartTimer和StopTimer本身也會帶來耗時,而且在納秒級別的測試中會顯得非常明顯
go test -bench='BenchmarkFib$|BenchmarkFibWithClean' benchmark-demo
goos: darwin
goarch: arm64
pkg: benchmark-demo
BenchmarkFib-8                       325           3631020 ns/op
BenchmarkFibWithClean-8              241           7448678 ns/op
PASS
ok      benchmark-demo  7.751s

危險用法,提前避開

  • 現在我們們對benchmark的瞭解已經比較全面了,可以覆蓋大多數單元測試場景,下面有兩個反面教材,希望我們們將來都能提前避免類似錯誤
  • 這兩個反面教材比較類似:對b.N的錯誤使用
  • 第一個錯誤用法如下所示,在執行b.N次迴圈的時候,將當前是第幾次作為入參傳入了被測試的方法fib
// BenchmarkFibWrongA 演示了錯誤的基準測試程式碼,這樣的測試可能無法結束
func BenchmarkFibWrongA(b *testing.B) {
	for n := 0; n < b.N; n++ {
		fib(n)
	}
}
  • 上述程式碼在基準測試的時候可能永遠不會結束,這是因為b.N的值並不固定,可能超出了fib方法的設計範圍,這樣就導致出現意料之外的結果(本意是效能測試,fib的入參應該是設計範圍內的),實際執行效果如下,紅色箭頭指向的狀態一直在等待中,只能強行關閉了
    在這裡插入圖片描述
  • 第二種反面教材也類似,不過更簡單,直接拿b.N作為入參,只呼叫一次fib方法,程式碼如下所示
func BenchmarkFibWrongB(b *testing.B) {
	fib(b.N)
}
  • 和前面的BenchmarkFibWrongA比,fib的執行次數似乎少了,但是請注意:b.N到底是多少呢?是否在fib方法的設計範圍內?依舊沒有明確答案,因此,程式碼也有可能永遠不會結束
  • 以本例中的fib為例,實際功能是斐波那契數列,我這邊入參等於50的時候,fib方法的耗時是54秒,所以,如果b.N的值再大一些,例如等於100的時候,fib方法就要計算很久了,而計算較大值並不是我們做基準測試的意圖
  • 至此,Go語言基準測試(benchmark)三部曲就全部完成了,相信此刻的您對除了信心滿滿,還有就是迫不及待的想去寫上一段benchmark程式碼,看看自己的方法函式究竟效能如何吧
  • 希望這三篇文章能給您帶來一些參考,golang學習路上,欣宸一路相伴

歡迎關注部落格園:程式設計師欣宸

學習路上,你不孤單,欣宸原創一路相伴...

相關文章