Golang基準測試

語言: CN / TW / HK
    • 3、傳入cpu num進行測試
    • 4、count多次執行基準測試
    • 5、benchtime指定執行秒數
    • 6、ResetTimer重置定時器
    • 7、benchmem展示記憶體消耗

1、基本使用

基準測試常用於程式碼效能測試,函式需要匯入 testing 包,並定義以 Benchmark 開頭的函式, 引數為 testing.B 指標型別,在測試函式中迴圈呼叫函式多次

go test testcalc/calc -bench .
go test testcalc/calc -bench . -run=none
# 顯示記憶體資訊
go test testcalc/calc -bench . -benchmem
go test -bench=. -benchmem -run=none

go test 會在執行基準測試之前之前執行包裡所有的單元測試,所有如果你的包裡有很多單元測試,或者它們會執行很長時間,你也可以通過 go test-run 標識排除這些單元測試

業務程式碼 fib.go ,測試斐波那契數列

package pkg06

func fib(n int) int {
	if n == 0 || n == 1 {
		return n
	}
	return fib(n-2) + fib(n-1)
}

測試程式碼 fib_test.go

package pkg06

import "testing"

func BenchmarkFib(b *testing.B) {
	for n := 0; n < b.N; n++ {
		fib(30)
	}
}

執行測試

➜  go test -bench=. -run=none
goos: darwin
goarch: amd64
pkg: pkg06
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkFib-12              250           4682682 ns/op
PASS
ok      pkg06   1.875s
➜  go test -bench=. -benchmem -run=none
goos: darwin
goarch: amd64
pkg: pkg06
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkFib-12              249           4686452 ns/op               0 B/op          0 allocs/op
PASS
ok      pkg06   1.854s

2、bench的工作原理

  • 基準測試函式會被一直呼叫直到 b.N 無效,它是基準測試迴圈的次數
  • b.N1 開始,如果基準測試函式在 1 秒內就完成 (預設值),則 b.N 增加,並再次執行基準測試函式
  • b.N 的值會按照序列 1,2,5,10,20,50,... 增加,同時再次執行基準測測試函式
  • 上述結果解讀代表 1 秒內運行了 250 次,每次 4682682 ns
  • -12 字尾和用於執行次測試的 GOMAXPROCS 值有關。 與 GOMAXPROCS 一樣,此數字預設為啟動時 Go 程序可見的 CPU 數。 可以使用 -cpu 標識更改此值,可以傳入多個值以列表形式來執行基準測試

3、傳入cpu num進行測試

➜  go test -bench=. -cpu=1,2,4  -benchmem -run=none
goos: darwin
goarch: amd64
pkg: pkg06
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkFib                 244           4694667 ns/op               0 B/op          0 allocs/op
BenchmarkFib-2               255           4721201 ns/op               0 B/op          0 allocs/op
BenchmarkFib-4               256           4756392 ns/op               0 B/op          0 allocs/op
PASS
ok      pkg06   5.826s

4、count多次執行基準測試

因為熱縮放、記憶體區域性性、後臺處理、 gc 活動等等會導致單次的誤差,所以一般會進行多次測試

➜  go test -bench=. -count=10  -benchmem -run=none 
goos: darwin
goarch: amd64
pkg: pkg06
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkFib-12              217           5993577 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              246           5065577 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              244           4955397 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              255           4689529 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              254           4879802 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              254           4691213 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              255           4772108 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              240           4724141 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              255           4717087 ns/op               0 B/op          0 allocs/op
BenchmarkFib-12              255           4787803 ns/op               0 B/op          0 allocs/op
PASS
ok      pkg06   18.166s

5、benchtime指定執行秒數

有的函式比較慢,為了更精確的結果,可以通過 -benchtime 標誌指定執行時間,從而使它執行更多次

➜  go test -bench=. -benchtime=5s  -benchmem -run=none
goos: darwin
goarch: amd64
pkg: pkg06
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkFib-12             1128           4716535 ns/op               0 B/op          0 allocs/op
PASS
ok      pkg06   7.199s

6、ResetTimer重置定時器

可能在真正測試之前還需要做很多例如初始化等工作,這時可以在需要測試的函式執行之初新增一個重置定時器的功能,這樣最終得到的時間就更為精確

package pkg06

import (
	"testing"
	"time"
)

func BenchmarkFib(b *testing.B) {
	time.Sleep(3 * time.Second)
	b.ResetTimer()
	for n := 0; n < b.N; n++ {
		fib(30)
	}
}

執行測試

➜  go test -bench=. -benchtime=5s  -benchmem -run=none
goos: darwin
goarch: amd64
pkg: pkg06
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkFib-12             1239           4712413 ns/op               0 B/op          0 allocs/op
PASS
ok      pkg06   16.122s

7、benchmem展示記憶體消耗

  • 例如測試大 cap 的切片,直接用 cap 初始化和 cap 動態擴容進行對比
package pkg07

import (
	"math/rand"
	"testing"
	"time"
)

// 指定大的cap的切片
func generateWithCap(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0, n)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}

// 動態擴容的slice
func generateDynamic(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}

func BenchmarkGenerateWithCap(b *testing.B) {
	for n := 0; n < b.N; n++ {
		generateWithCap(100000)
	}
}

func BenchmarkGenerateDynamic(b *testing.B) {
	for n := 0; n < b.N; n++ {
		generateDynamic(100000)
	}
}

執行測試

➜  go test -bench=. -benchmem -run=none 
goos: darwin
goarch: amd64
pkg: pkg07
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkGenerateWithCap-12          672           1729465 ns/op          802817 B/op          1 allocs/op
BenchmarkGenerateDynamic-12          561           2122992 ns/op         4654346 B/op         30 allocs/op
PASS
ok      pkg07   3.777s

結論:用 cap 初始化好的效能可以高一個數據量級

  • 例如測試測試函式複雜度,不帶 capslice 動態擴容

對上面程式碼中呼叫動態擴容生成切片進行再次封裝

package pkg08

import (
	"math/rand"
	"testing"
	"time"
)

// 指定大的cap的切片
func generateWithCap(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0, n)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}

// 動態擴容的slice
func generateDynamic(n int) []int {
	rand.Seed(time.Now().UnixNano())
	nums := make([]int, 0)
	for i := 0; i < n; i++ {
		nums = append(nums, rand.Int())
	}
	return nums
}

func benchmarkGenerate(i int, b *testing.B) {
	for n := 0; n < b.N; n++ {
		generateDynamic(i)
	}
}

func BenchmarkGenerateDynamic1000(b *testing.B)     { benchmarkGenerate(1000, b) }
func BenchmarkGenerateDynamic10000(b *testing.B)    { benchmarkGenerate(10000, b) }
func BenchmarkGenerateDynamic100000(b *testing.B)   { benchmarkGenerate(100000, b) }
func BenchmarkGenerateDynamic1000000(b *testing.B)  { benchmarkGenerate(1000000, b) }
func BenchmarkGenerateDynamic10000000(b *testing.B) { benchmarkGenerate(10000000, b) }

執行測試

➜  go test -bench=. -benchmem -run=none 
goos: darwin
goarch: amd64
pkg: pkg08
cpu: Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz
BenchmarkGenerateDynamic1000-12            39540             26557 ns/op           16376 B/op         11 allocs/op
BenchmarkGenerateDynamic10000-12            5452            210894 ns/op          386296 B/op         20 allocs/op
BenchmarkGenerateDynamic100000-12            572           2106325 ns/op         4654341 B/op         30 allocs/op
BenchmarkGenerateDynamic1000000-12            48          23070939 ns/op        45188416 B/op         40 allocs/op
BenchmarkGenerateDynamic10000000-12            5         212567041 ns/op        423503110 B/op        50 allocs/op
PASS
ok      pkg08   9.686s

結論:輸入變為原來的 10 倍,單次耗時也差不多是上一級的 10 倍。說明這個函式的複雜度是接近線性的