基準測試(benchmark)是 go testing 庫提供的,用來度量程式性能,算法優劣的利器。在日常生活中,我們使用速度 m/s(機關時間内物體移動的距離)大小來衡量一輛跑車的性能,同理,我們可以使用”機關時間内程式運作的次數“來衡量程式的性能。
前言
基準測試(benchmark)是 go testing 庫提供的,用來度量程式性能,算法優劣的利器。
在日常生活中,我們使用速度 m/s(機關時間内物體移動的距離)大小來衡量一輛跑車的性能,同理,我們可以使用”機關時間内程式運作的次數“來衡量程式的性能。
在日常開發中,如果和同僚在代碼實作上有分歧,不用多費口舌,跑個分就知道誰牛X。
注意:在進行基準測試時,硬體資源直接影響測試結果,為了保證測試結果的可重複性,需要盡可能地保證硬體資源一緻。(單一變量原則)
快速開始
建立項目
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 ./... |
-
指令預設不執行 benchmark 測試,需要加上go test
參數,該參數支援正規表達式,隻有比對到的測試用例才會執行,使用-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 即
,預設等于 CPU 核數GOMAXPROCS
-
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