「譯」更詳細的 Go 效能測試
我一直在優化我的 go 程式碼並且一直優化我的效能測試方案。
讓我們先看一個簡單的例子:
func BenchmarkReport(b *testing.B) { runtime.GC() for i := 0; i < b.N; i++ { r := fmt.Sprintf("hello, world %d", 123) runtime.KeepAlive(r) } }
執行 go test -beach .
會看到這樣子的結果:
BenchmarkReport-3220000000107 ns/op
這可能可以初略的估計效能表現,但是徹底的優化需要更詳細的結果。
將所有的內容壓縮成一個數字必然是簡單的。
讓我向你們介紹我寫的 hrtime 包,以便於獲取更詳細的效能測試結果。
直方圖
第一個推薦使用的是 hrtime.NewBeachmark
,重寫上面的簡單例子:
func main() { bench := hrtime.NewBenchmark(20000000) for bench.Next() { r := fmt.Sprintf("hello, world %d", 123) runtime.KeepAlive(r) } fmt.Println(bench.Histogram(10)) }
它會輸出:
avg 372ns;min 300ns;p50 400ns;max 295µs; p90 400ns;p99 500ns;p999 1.8µs;p9999 4.3µs; 300ns [ 7332554] ███████████████████████ 400ns [12535735] ████████████████████████████████████████ 600ns [18955] 800ns [2322] 1µs [20413] 1.2µs [34854] 1.4µs [25096] 1.6µs [10009] 1.8µs [4688] 2µs+[15374]
我們可以看出 P99 是 500ns,表示的是 1% 的測試超過 500ns,我們可以分配更小的字串來優化:
func main() { bench := hrtime.NewBenchmark(20000000) var back [1024]byte for bench.Next() { buffer := back[:0] buffer = append(buffer, []byte("hello, world ")...) buffer = strconv.AppendInt(buffer, 123, 10) runtime.KeepAlive(buffer) } fmt.Println(bench.Histogram(10)) }
結果如下:
avg 267ns;min 200ns;p50 300ns;max 216µs; p90 300ns;p99 300ns;p999 1.1µs;p9999 3.6µs; 200ns [ 7211285] ██████████████████████▌ 300ns [12658260] ████████████████████████████████████████ 400ns [81076] 500ns [3226] 600ns [343] 700ns [136] 800ns [729] 900ns [8108] 1µs [15436] 1.1µs+[21401]
現在可以看到 99% 的測試已經從 500ns 降到了 300ns。
如果你眼神犀利,可能已經注意到 go beachmark 給出了 107ns/op 但是 hrtime 給了 372ns/op。
這是獲取更多測試資訊的副作用,他們總是會有開銷的。最終結果包括這種開銷。
Stopwatch
有時候我們還行測試併發操作,這時候可能需要 Stopwatch。
假如你想在測試一個多競爭 channel 的持續時間。當然這是一個認為的例子,大致描述瞭如何從一個 goroutine 開始在另一個 goroutine 結束並且列印結果。
func main() { const numberOfExperiments = 1000 bench := hrtime.NewStopwatch(numberOfExperiments) ch := make(chan int32, 10) wait := make(chan struct{}) // start senders for i := 0; i < numberOfExperiments; i++ { go func() { <-wait ch <- bench.Start() }() } // start one receiver go func() { for lap := range ch { bench.Stop(lap) } }() // wait for all goroutines to be created time.Sleep(time.Second) // release all goroutines at the same time close(wait) // wait for all measurements to be completed bench.Wait() fmt.Println(bench.Histogram(10)) }
hrtesting
當然重寫所有的測試用例是不現實的。為此有 github.com/loov/hrtime/hrtesting
為測試提供 testing.B
。
func BenchmarkReport(b *testing.B) { bench := hrtesting.NewBenchmark(b) defer bench.Report() for bench.Next() { r := fmt.Sprintf("hello, world %d", 123) runtime.KeepAlive(r) } }
會打印出 P50、P90、P99:
BenchmarkReport-323000000427 ns/op --- BENCH: BenchmarkReport-32 benchmark_old.go:11: 24.5µs₅₀ 24.5µs₉₀ 24.5µs₉₉ N=1 benchmark_old.go:11:400ns₅₀500ns₉₀ 12.8µs₉₉ N=100 benchmark_old.go:11:400ns₅₀500ns₉₀500ns₉₉ N=10000 benchmark_old.go:11:400ns₅₀500ns₉₀600ns₉₉ N=1000000 benchmark_old.go:11:400ns₅₀500ns₉₀500ns₉₉ N=3000000
在 Go 1.12 中將會打印出所有的 Beachmark 而不是最後一個,但是在 Go 1.13 中可以輸出的更好:
BenchmarkReport-323174566379 ns/op400 ns/p50400 ns/p90 ...
獲得的結果也可以和 beachstat 進行比較。
hrpolt
最後載介紹一下 github.com/loov/hrtime/hrplot
,使用我實驗性質的繪圖包,我決定新增一種方便的方法來繪製測試結果。
func BenchmarkReport(b *testing.B) { bench := hrtesting.NewBenchmark(b) defer bench.Report() defer hrplot.All("all.svg", bench) runtime.GC() for bench.Next() { r := fmt.Sprintf("hello, world %d", 123) runtime.KeepAlive(r) } }
將會建立一個 SVG 檔案 all.svg
。其中包括線性圖,顯示了每次迭代所花費的時間;第二個就是密度圖,顯示了測量時間的分佈圖,以及最後一個百分位的詳情。
Conclusion
效能優化很有趣,但是有更好的根據可以變得更加有趣。
去嘗試 github.com/loov/hrtime 讓我知道你更多的想法。