基準測試(benchmark)是 go testing 庫提供的,用來度量程序性能,算法優劣的利器。html
在平常生活中,咱們使用速度 m/s(單位時間內物體移動的距離)大小來衡量一輛跑車的性能,同理,咱們可使用」單位時間內程序運行的次數「來衡量程序的性能。linux
在平常開發中,若是和同事在代碼實現上有分歧,不用多費口舌,跑個分就知道誰牛X。golang
注意:在進行基準測試時,硬件資源直接影響測試結果,爲了保證測試結果的可重複性,須要儘量地保證硬件資源一致。(單一變量原則)正則表達式
建立項目 learnGolang
算法
mkdir learnGolang cd learnGolang go mod init learnGolang
建立文件 main.go
,編寫咱們的被測函數shell
package main // 斐波那契數列 func fib(n int) int { if n < 2 { return n } return fib(n-1) + fib(n-2) } func sum(a, b int) int { return a + b }
建立文件 main_test.go
,編寫基準測試用例編程
package main import "testing" func BenchmarkFib10(b *testing.B) { for n := 0; n < b.N; n++ { fib(10) } } func BenchmarkFib20(b *testing.B) { for n := 0; n < b.N; n++ { fib(20) } } func BenchmarkSum(b *testing.B) { for n := 0; n < b.N; n++ { sum(1, 2) } }
package
內的測試文件以 _test.go
結尾,其中的測試用例格式爲 func BenchmarkXxx(b *testing.B)
,注意 Xxx
首字母要大寫(即駝峯命名法)開始運行windows
$ go test -bench=. . goos: windows goarch: amd64 pkg: learnGolang BenchmarkFib10-4 3360627 362 ns/op BenchmarkFib20-4 26676 44453 ns/op BenchmarkSum-4 1000000000 0.296 ns/op PASS ok learnGolang 3.777s
go test [packages]
指定測試範圍方法一 | 方法二 | |
---|---|---|
運行當前 package 內的用例 | go test packageName | go test . |
運行子 package 內的用例 | go test packageName/subName | go test ./subName |
遞歸運行全部的用例 | go test packageName/... | go test ./... |
go test
命令默認不執行 benchmark 測試,須要加上 -bench
參數,該參數支持正則表達式,只有匹配到的測試用例纔會執行,使用 .
則運行全部測試用例# 只運行斐波那契數列測試用例 $ go test -bench='.*Fib.*' . goos: windows goarch: amd64 pkg: learnGolang BenchmarkFib10-4 3287449 357 ns/op BenchmarkFib20-4 27097 44461 ns/op PASS ok learnGolang 3.418s
GOMAXPROCS
,默認等於 CPU 核數3287449 357 ns/op
表示單位時間內(默認是1s)被測函數運行了 3287449 次,每次運行耗時 357ns,app
3287449*357ns=1.173s(耗時比 1s 略多,由於測試用例執行、銷燬等是須要時間的)函數
ok learnGolang 3.418s
表示本次測試總耗時
在高中物理學中,因爲測試物體瞬時速度很差實現,咱們可讓物體多移動一段時間,而後採用「總距離/時間段」算出平均速度來代替瞬時速度。
go benchmark 默認測試時間是 1s,一樣的原理,爲了提高測試準確度,咱們可使用該參數適當增長時長。
➜ learnGolang go test -bench='Fib10$' goos: linux goarch: amd64 pkg: learnGolang BenchmarkFib10-12 4153650 288 ns/op PASS ok learnGolang 1.491s # 指定時長爲 5s ➜ learnGolang go test -bench='Fib10$' -benchtime=5s goos: linux goarch: amd64 pkg: learnGolang BenchmarkFib10-12 20616992 288 ns/op PASS ok learnGolang 6.235s
仍是高中物理學,咱們也能夠指定物理移動的距離,而後測量所耗費的時間,計算平均速度。
該參數還支持特殊的形式 Nx
,用來指定被測程序的運行次數。
# 指定運行次數爲 1000 次 ➜ learnGolang go test -bench='Fib10$' -benchtime=1000x goos: linux goarch: amd64 pkg: learnGolang BenchmarkFib10-12 1000 305 ns/op PASS ok learnGolang 0.002s
一樣相似與測量物體速度,爲了提高精確度,咱們多作幾回測試。
➜ learnGolang go test -bench='Fib10$' -benchtime=5s -count=3 goos: linux goarch: amd64 pkg: learnGolang BenchmarkFib10-12 19596388 288 ns/op BenchmarkFib10-12 20796957 290 ns/op BenchmarkFib10-12 20492478 291 ns/op PASS ok learnGolang 18.542s
該參數能夠設置 benchmark 所使用的 CPU 核數。
下面咱們模擬一次多核並行計算的例子,並觀察設置不一樣核數後的測試結果
// main.go func parallelExam() int { chs := make([]chan int, 10) // 設置 10 個協程去並行計算 for i := 0; i < len(chs); i++ { chs[i] = make(chan int, 1) go parallelSum(chs[i]) } sum := 0 for _, ch := range chs { res := <-ch sum += res } return sum } func parallelSum(ch chan int) { defer close(ch) sum := 0 for i := 1; i <= 100000; i++ { // 10萬 sum += i } ch <- sum }
// main_test.go func BenchmarkParallelExam(b *testing.B) { for n := 0; n < b.N; n++ { parallelExam() } }
➜ learnGolang go test -bench='BenchmarkParallelExam' -cpu=1,4,6,10,12 goos: linux goarch: amd64 pkg: learnGolang BenchmarkParallelExam 3154 366754 ns/op BenchmarkParallelExam-4 9316 119747 ns/op BenchmarkParallelExam-6 10000 107040 ns/op BenchmarkParallelExam-10 10000 108144 ns/op BenchmarkParallelExam-12 9891 110018 ns/op PASS ok learnGolang 5.604s
從運行結果看出,隨着 CPU 核數的增長,性能逐步提高,可是到必定閾值後,性能趨於穩定,此時再增長 CPU 核數,性能反而降低,由於 CPU 核心之間的切換也是須要成本的。
除了速度,內存分配狀況也是須要咱們重點關注的指標。
go 語言中,slice
有一個 cap
屬性,合理的設置該值,能夠減小內存分配次數,分配大小,提高程序性能。
// main.go func sliceNoCap() { s := make([]int, 0) // 未設置 cap 值 for i := 0; i < 10000; i++ { s = append(s, i) } } func sliceWithCap() { s := make([]int, 0, 10000) // 預先設置 cap 值 for i := 0; i < 10000; i++ { s = append(s, i) } }
// main_test.go func BenchmarkSliceNoCap(b *testing.B) { for n := 0; n < b.N; n++ { sliceNoCap() } } func BenchmarkSliceWithCap(b *testing.B) { for n := 0; n < b.N; n++ { sliceWithCap() } }
➜ learnGolang go test -bench='Cap$' -benchmem . goos: linux goarch: amd64 pkg: learnGolang BenchmarkSliceNoCap-12 31318 38614 ns/op 386297 B/op 20 allocs/op BenchmarkSliceWithCap-12 111764 10269 ns/op 81920 B/op 1 allocs/op PASS ok learnGolang 2.858s
能夠看到前者每次執行會分配 386297 字節的內存,約等於後者的 3.76 倍,每次執行會分配內存 20 次,是後者的 20 倍。
If a benchmark needs some expensive setup before running, the timer may be reset
若是在整個 benchmark 執行前,須要一些耗時的準備工做,咱們須要將這部分耗時忽略掉
func BenchmarkFib(b *testing.B) { time.Sleep(3 * time.Second) // 模擬耗時的準備工做 b.ResetTimer() // 重置計時器,忽略前面的準備時間 for n := 0; n < b.N; n++ { fib(10) } }
StopTimer stops timing a test. This can be used to pause the timer while performing complex initialization that you don't want to measure.
StartTimer starts timing a test. This function is called automatically before a benchmark starts, but it can also be used to resume timing after a call to StopTimer.
若是在被測函數每次執行前,須要一些準備工做,咱們可使用 StopTimer
暫停計時,準備工做完成後,使用 StartTimer
繼續計時。
func BenchmarkFib(b *testing.B) { for n := 0; n < b.N; n++ { b.StopTimer() // 暫停計時 prepare() // 每次函數執行前的準備工做 b.StartTimer() // 繼續計時 funcUnderTest() // 被測函數 } }