Go benchmark 詳解


20210216141958

前言

基准測試(benchmark)是 go testing 庫提供的,用來度量程序性能,算法優劣的利器。

在日常生活中,我們使用速度 m/s(單位時間內物體移動的距離)大小來衡量一輛跑車的性能,同理,我們可以使用”單位時間內程序運行的次數“來衡量程序的性能。

在日常開發中,如果和同事在代碼實現上有分歧,不用多費口舌,跑個分就知道誰牛X。

run

注意:在進行基准測試時,硬件資源直接影響測試結果,為了保證測試結果的可重復性,需要盡可能地保證硬件資源一致。(單一變量原則)

快速開始

創建項目 learnGolang

mkdir learnGolang
cd learnGolang
go mod init learnGolang

創建文件 main.go,編寫我們的被測函數

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 首字母要大寫(即駝峰命名法)
  • 函數內被測函數循環執行 b.N 次

開始運行

$ 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
  • BenchmarkFib10-4 中的 4 即 GOMAXPROCS,默認等於 CPU 核數

image-20210218095320023

  • 3287449 357 ns/op 表示單位時間內(默認是1s)被測函數運行了 3287449 次,每次運行耗時 357ns,

    3287449*357ns=1.173s(耗時比 1s 略多,因為測試用例執行、銷毀等是需要時間的)

  • ok learnGolang 3.418s 表示本次測試總耗時

進階參數

-benchtime t

在高中物理學中,由於測試物體瞬時速度不好實現,我們可以讓物體多移動一段時間,然后采用“總距離/時間段”算出平均速度來代替瞬時速度。

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

-count n

同樣類似與測量物體速度,為了提升精確度,我們多做幾次測試。

➜  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

-cpu n

該參數可以設置 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 核心之間的切換也是需要成本的。

-benchmem

除了速度,內存分配情況也是需要我們重點關注的指標。

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 倍。

注意事項

ResetTimer

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 & StartTimer

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() // 被測函數
	}
}

參考

Go 語言高性能編程 - benchmark 基准測試

Go Package Testing

Go Testing flags

High Performance Go Workshop


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM