原文連接: https://github.com/sxs2473/go...
本文使用 Creative Commons Attribution-ShareAlike 4.0 International 協議進行受權許可。
本節重點討論如何使用 Go 測試框架構建一個有效的基準測試,並提供一些實用的技巧來避免性能缺陷。git
在進行基準測試以前,咱們必需要有一個穩定的環境來得到可重現的結果。github
若是你負擔得起,最好購買專用的性能測試硬件。並禁用全部電源管理和熱縮放,保持機器上的軟件版本不變。golang
對於其餘人,請使用先後樣本並屢次運行它們以得到一致的結果。正則表達式
testing
包已經內置了支持基準測試的能力. 好比你有一個簡單的函數:算法
// 此函數計算斐波那契數列中第 N 個數字 func Fib(n int) int { switch n { case 0: return 0 case 1: return 1 default: return Fib(n-1) + Fib(n-2) } }
咱們可使用 testing
包以以下形式爲此函數寫一個基準測試。基準測試函數也寫在以 _test.go
結尾的文件裏,它和test
函數共存.服務器
func BenchmarkFib20(b *testing.B) { for n := 0; n < b.N; n++ { Fib(20) // 運行 Fib 函數 N 次 } }
基準測試和普通單元測試相似。 惟一的區別是基準測試接收的參數是*testing.B
而不是 *testing.T
。 這兩種類型都實現了 testing.TB
接口,這個接口提供了一些比較經常使用的方法 Errorf()
, Fatalf()
, and FailNow()
。架構
由於基準測試使用testing
包,它們一樣經過 go test 命令執行。可是,默認狀況下,當你調用go test
時,基準測試是不執行的。框架
要顯式地執行基準測試請使用 -bench
標識。 -bench
接收一個與待運行的基準測試名稱相匹配的正則表達式,所以,若是要運行包中全部的基準測試,最多見的方法是這樣寫 -bench=.
。例如:函數
% go test -bench=. ./examples/fib/ goos: darwin goarch: amd64 BenchmarkFib20-8 30000 44514 ns/op PASS ok _/Users/dfc/devel/gophercon2018-performance-tuning-workshop/2-benchmarking/examples/fib 1.795s
注意 : go test
會在運行基準測試以前以前執行包裏全部的單元測試,全部若是你的包裏有不少單元測試,或者它們會運行很長時間,你也能夠經過 go test
的-run
標識排除這些單元測試,不讓它們執行; 好比: go test -run=^$
。工具
基準測試函數會被一直調用直到b.N
無效,它是基準測試循環的次數
b.N
從 1 開始,若是基準測試函數在1秒內就完成 (默認值),則 b.N
增長,並再次運行基準測試函數。
b.N
在近似這樣的序列中不斷增長;1, 2, 3, 5, 10, 20, 30, 50, 100 等等。 基準框架試圖變得聰明,若是它看到當b.N
較小並且測試很快就完成的時候,它將讓序列增長地更快。
看上面的例子, BenchmarkFib20-8
發現約 30000 次迭代只須要1秒鐘。 From there the benchmark framework computed that
注意 : The -8
後綴和用於運行次測試的 GOMAXPROCS
值有關。 與GOMAXPROCS
同樣,此數字默認爲啓動時Go進程可見的CPU數。 你可使用-cpu
標識更改此值,能夠傳入多個值以列表形式來運行基準測試。
% go test -bench=. -cpu=1,2,4 ./examples/fib/ goos: darwin goarch: amd64 BenchmarkFib20 30000 44644 ns/op BenchmarkFib20-2 30000 44504 ns/op BenchmarkFib20-4 30000 44848 ns/op PASS
fib
函數是一個模擬的例子 — 除非你編寫 TechPower 服務器基準測試來驗證,不然你的業務不太多是你計算斐波那契數列中第20個數字的速度。 可是,基準確實展示了我認爲有效的基準。
具體來講,當你的基準測試運行幾千次迭代的時候,咱們能夠認爲得到了一個每次運行的平均值,而若是基準測試只運行幾十次,那麼這個平均值極可能不穩定,也就不能說明問題。
要增長迭代次數,可使用-benchtime
標識增長運行時間,例如
% go test -bench=. -benchtime=10s ./examples/fib/ goos: darwin goarch: amd64 BenchmarkFib20-8 300000 44616 ns/op
運行一個相同的基準測試,直到它到達b.N
的值,運行時間超過10秒。當咱們運行時間是10倍的時候,迭代次數也會增長到10倍。然而每一次執行的結果卻沒有什麼變化,這正是咱們所預期的。
若是你有一個基準測試,它運行數百萬次或數十億次迭代,每次操做的時間都在微秒或納秒級,那麼你可能會發現基準測試結果不穩定,由於熱縮放、內存局部性、後臺處理、gc活動等等。
對於每次操做是以10或個位數納秒爲單位計算的函數來講,指令從新排序和代碼對齊的相對效應都將對結果產生影響。
可使用-count
標識屢次運行基準測試來解決這個問題:
% go test -bench=Fib1 -count=10 ./examples/fib/ goos: darwin goarch: amd64 BenchmarkFib1-8 2000000000 1.99 ns/op BenchmarkFib1-8 1000000000 1.95 ns/op BenchmarkFib1-8 2000000000 1.99 ns/op BenchmarkFib1-8 2000000000 1.97 ns/op BenchmarkFib1-8 2000000000 1.99 ns/op BenchmarkFib1-8 2000000000 1.96 ns/op BenchmarkFib1-8 2000000000 1.99 ns/op BenchmarkFib1-8 2000000000 2.01 ns/op BenchmarkFib1-8 2000000000 1.99 ns/op BenchmarkFib1-8 1000000000 2.00 ns/op
得出Fib(1)
的基準測試在2納秒左右,方差爲正負2%.
提示 : 若是你發現須要針對特定的包調整不一樣的默認值,我建議使用Makefile
中完成這些設定,這樣每一個想要運行基準測試的人均可以使用相同的配置進行編碼。
在上一節中,我建議屢次運行基準測試以得到更多的平均數據。對於任何基準測試來講,這都是一個很好的建議,由於測試過程會受到電源管理、後臺進程和熱管理的影響,這個問題我在本章的開頭已經提到過。
下面我將介紹一個由 Russ Cox 編寫的測試工具 benchstat
% go get golang.org/x/perf/cmd/benchstat
Benchstat 能夠獲取一組基準測試數據,並告訴你它的穩定性如何。如下是使用電池時的數據:
% go test -bench=Fib20 -count=10 ./examples/fib/ | tee old.txt goos: darwin goarch: amd64 BenchmarkFib20-8 30000 46295 ns/op BenchmarkFib20-8 30000 41589 ns/op BenchmarkFib20-8 30000 42204 ns/op BenchmarkFib20-8 30000 43923 ns/op BenchmarkFib20-8 30000 44339 ns/op BenchmarkFib20-8 30000 45340 ns/op BenchmarkFib20-8 30000 45754 ns/op BenchmarkFib20-8 30000 45373 ns/op BenchmarkFib20-8 30000 44283 ns/op BenchmarkFib20-8 30000 43812 ns/op PASS ok _/Users/dfc/devel/gophercon2018-performance-tuning-workshop/2-benchmarking/examples/fib 17.865s % benchstat old.txt name time/op Fib20-8 44.3µs ± 6%
benchstat
告訴咱們,平均值爲44.3微秒,樣本間的波動區間爲正負 6%。 這對電池電量來講在乎料之中。
肯定兩組基準測試結果之間的差別多是單調乏味且容易出錯的。 Benchstat 能夠幫助咱們解決這個問題。
提示 : 保存基準運行的輸出頗有用,但你也能夠保存生成它的二進制文件。 爲此,請使用-c
標誌來保存測試二進制文件;我常常將這個二進制文件從.test
重命名爲.golden
。
% go test -c % mv fib.test fib.golden
Fib
性能先前的Fib
函數對斐波納契數列中的第0和第1個數字進行了硬編碼。 以後,代碼以遞歸方式調用自身。 咱們將在後邊討論遞歸的代價,但目前,假設它有代價,特別當咱們的算法是指數級複雜度的時候。
要解決這個問題,最簡單的方法就是硬編碼斐波那契數列中的另外一個數字,將每次調用的深度減小一個。
func Fib(n int) int { switch n { case 0: return 0 case 1: return 1 case 2: return 1 default: return Fib(n-1) + Fib(n-2) } }
爲了比較咱們的新版本,咱們編譯了一個新的測試二進制文件並對它們都進行了基準測試,並使用benchstat
對輸出進行比較。
% go test -c % ./fib.golden -test.bench=. -test.count=10 > old.txt % ./fib.test -test.bench=. -test.count=10 > new.txt % benchstat old.txt new.txt name old time/op new time/op delta Fib20-8 44.3µs ± 6% 25.6µs ± 2% -42.31% (p=0.000 n=10+10)
比較基準測試時須要檢查三件事
-count=10
。拒絕率小於10%通常是沒問題的,而高於10%可能代表你的設置是不穩定的,也多是比較的樣本太少了。有時候每次基準測試運行前都有一些初始化操做。 b.ResetTimer()
將讓你跳過這些運行時間。
func BenchmarkExpensive(b *testing.B) { boringAndExpensiveSetup() b.ResetTimer() // HL for n := 0; n < b.N; n++ { // 被測試的功能 } }
若是每次循環迭代內部都有一些高成本的其餘邏輯,請使用b.StopTimer()
和b.StartTimer()
來暫停基準計時器。
func BenchmarkComplicated(b *testing.B) { for n := 0; n < b.N; n++ { b.StopTimer() // HL complicatedSetup() b.StartTimer() // HL // 被測試的功能 } }
分配計數和大小與基準測試的執行時間密切相關。 你能夠告訴測試框架記錄被測代碼所作的分配數量。
func BenchmarkRead(b *testing.B) { b.ReportAllocs() for n := 0; n < b.N; n++ { // 被測試的功能 } }
如下是使用bufio軟件包基準測試的示例:
% go test -run=^$ -bench=. bufio goos: darwin goarch: amd64 pkg: bufio BenchmarkReaderCopyOptimal-8 20000000 103 ns/op BenchmarkReaderCopyUnoptimal-8 10000000 159 ns/op BenchmarkReaderCopyNoWriteTo-8 500000 3644 ns/op BenchmarkReaderWriteToOptimal-8 5000000 344 ns/op BenchmarkWriterCopyOptimal-8 20000000 98.6 ns/op BenchmarkWriterCopyUnoptimal-8 10000000 131 ns/op BenchmarkWriterCopyNoReadFrom-8 300000 3955 ns/op BenchmarkReaderEmpty-8 2000000 789 ns/op 4224 B/op 3 allocs/op BenchmarkWriterEmpty-8 2000000 683 ns/op 4096 B/op 1 allocs/op BenchmarkWriterFlush-8 100000000 17.0 ns/op 0 B/op 0 allocs/op
注意 : 想對全部基準測試都生效,你也可使用go test -benchmem
標識。
% go test -run=^$ -bench=. -benchmem bufio goos: darwin goarch: amd64 pkg: bufio BenchmarkReaderCopyOptimal-8 20000000 93.5 ns/op 16 B/op 1 allocs/op BenchmarkReaderCopyUnoptimal-8 10000000 155 ns/op 32 B/op 2 allocs/op BenchmarkReaderCopyNoWriteTo-8 500000 3238 ns/op 32800 B/op 3 allocs/op BenchmarkReaderWriteToOptimal-8 5000000 335 ns/op 16 B/op 1 allocs/op BenchmarkWriterCopyOptimal-8 20000000 96.7 ns/op 16 B/op 1 allocs/op BenchmarkWriterCopyUnoptimal-8 10000000 124 ns/op 32 B/op 2 allocs/op BenchmarkWriterCopyNoReadFrom-8 500000 3219 ns/op 32800 B/op 3 allocs/op BenchmarkReaderEmpty-8 2000000 748 ns/op 4224 B/op 3 allocs/op BenchmarkWriterEmpty-8 2000000 662 ns/op 4096 B/op 1 allocs/op BenchmarkWriterFlush-8 100000000 16.9 ns/op 0 B/op 0 allocs/op PASS ok bufio 20.366s
這個例子來自 issue 14813。
const m1 = 0x5555555555555555 const m2 = 0x3333333333333333 const m4 = 0x0f0f0f0f0f0f0f0f const h01 = 0x0101010101010101 func popcnt(x uint64) uint64 { x -= (x >> 1) & m1 x = (x & m2) + ((x >> 2) & m2) x = (x + (x >> 4)) & m4 return (x * h01) >> 56 } func BenchmarkPopcnt(b *testing.B) { for i := 0; i < b.N; i++ { popcnt(uint64(i)) } }
你以爲這個基準測試會有多快?讓咱們來看看。
% go test -bench=. ./examples/popcnt/ goos: darwin goarch: amd64 BenchmarkPopcnt-8 2000000000 0.30 ns/op PASS
0.3 納秒,這基本上是一個時鐘週期。即便假設CPU每一個時鐘週期內會執行多條指令,這個數字彷佛也不合理地低。 發生了什麼?
要了解發生了什麼,咱們必須看看benchmark下的函數popcnt。 popcnt是一個葉子函數 - 它不調用任何其餘函數 - 所以編譯器能夠內聯它。
由於函數是內聯的,因此編譯器如今能夠看到它沒有反作用。 popcnt不會影響任何全局變量的狀態。 這樣,調用就被消除了。 這是編譯器看到的:
func BenchmarkPopcnt(b *testing.B) { for i := 0; i < b.N; i++ { // 優化了 } }
在全部版本的Go編譯器上,仍然會生成循環。 可是英特爾CPU很是擅長優化循環,尤爲是空循環。
須要去掉的是,經過刪除沒必要要的計算使真正的代碼快速運行的優化,與刪除沒有明顯反作用的基準測試的優化是相同的。
隨着Go編譯器的改進,這隻會變得更加廣泛。
要修復此基準測試,咱們必須確保編譯器沒法檢驗BenchmarkPopcnt
的主體不會致使全局狀態發生變化。
var Result uint64 func BenchmarkPopcnt(b *testing.B) { var r uint64 for i := 0; i < b.N; i++ { r = popcnt(uint64(i)) } Result = r }
這是確保編譯器沒法優化循環體的推薦方法。
首先,咱們經過將調用popcnt
的結果存儲在r
中。 而後,當測試基準結束時,r
在BenchmarkPopcnt
的範圍內被聲明,r
的結果對於程序的另外一部分是不可見的,因此最終,咱們將r
值賦給包級別的公共變量Result
。
由於Result
是公共的,因此編譯器沒法證實導入此類的另外一個包將沒法看到Result
隨時間變化的值,所以它沒法優化致使其賦值的任何操做。
for
循環對基準測試的執行很是重要
下面是兩個錯誤的的基準測試例子:
func BenchmarkFibWrong(b *testing.B) { Fib(b.N) }
func BenchmarkFibWrong2(b *testing.B) { for n := 0; n < b.N; n++ { Fib(n) } }
結果是,它們會一直執行下去
testing
包內置了支持生成CPU,內存和塊的profile文件。
-cpuprofile=$FILE
將 CPU 分析結果寫入 $FILE
.-memprofile=$FILE
將內存分析結果寫入 $FILE
, -memprofilerate=N
調整記錄速率爲 1/N
.-blockprofile=$FILE
, 將塊分析結果寫入 $FILE
.使用這些標識中的任何一個同時都會保留二進制文件。
% go test -run=XXX -bench=. -cpuprofile=c.p bytes % go tool pprof c.p