Go 語言除了內建強大的測試工具 (go test) 之外,也提供了效能評估的工具 (go tool pprof),整個生態鏈非常完整,這也是我推薦大家使用 Go 語言的最大原因,這篇會介紹如何使用 pprof 來找出效能瓶頸的地方。假設開發者在寫任何邏輯功能時,發現跑出來的速度不是想像的這麼快,或者是在串接服務流程時,整個回覆時間特別久,這時候可以透過 benchmark 先找出原因。
go test -bench=. -benchtime=3s ./lexer/
可以看到底下輸出結果
BenchmarkCreateKeyString1-8 100000000 35.9 ns/op 8 B/op 1 allocs/op
BenchmarkCreateKeyString2-8 85222555 42.4 ns/op 8 B/op 1 allocs/op
BenchmarkCreateKeyString3-8 73403774 48.0 ns/op 8 B/op 1 allocs/op
從上面數據可以看到效能結果,開發者可以根據這結果來調教程式碼,改善過後再透過一樣的指令來評估是否有改善成功。我個人通常開一個新的 performance 分支來進行效能調校,調教完成後,再執行上面指令輸出到存文字檔
go test -bench=. -benchtime=3s ./lexer/ > new.txt
接著切回去 master 分支,用同樣的指令
go test -bench=. -benchtime=3s ./lexer/ > old.txt
接著用 benchstat 來看看是否有改善,改善了多少?
$ benchstat -alpha 3 a.txt b.txt
name old time/op new time/op delta
Lexer-8 3.43µs ± 0% 2.22µs ± 0% -35.23% (p=1.000 n=1+1)
name old speed new speed delta
Lexer-8 242MB/s ± 0% 373MB/s ± 0% +54.36% (p=1.000 n=1+1)
name old alloc/op new alloc/op delta
Lexer-8 896B ± 0% 896B ± 0% ~ (all equal)
name old allocs/op new allocs/op delta
Lexer-8 1.00 ± 0% 1.00 ± 0% ~ (all equal)
效能評估
上面方式來評估效能之外,最主要遇到的問題會是,在一大段程式碼及邏輯中,要找出慢的主因,就不能光是靠上面的方式,因為開發者不會知道整段程式碼到底慢在哪邊,100 行內要找出慢的原因很難,那 1000 行更難,所以需要透過其他方式來處理。這時候就要使用到 pprof 來找出程式碼所有執行的時間,怎麼輸出 CPU 所花的時間,可以透過底下指令:
go test -bench=. -benchtime=3s \
-cpuprofile cpu.out \
./lexer/
產生出 cpu.out
後,就可以使用 go
指令來看看哪邊出問題
go tool pprof cpu.out
可以進到 console 畫面:
$ go tool pprof cpu.out
Type: cpu
Time: Jun 7, 2020 at 11:26am (CST)
Duration: 6.04s, Total samples = 5.95s (98.56%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
接下來使用方式非常簡單,可以使用 top 功能來看數據
(pprof) top10
Showing nodes accounting for 4850ms, 81.51% of 5950ms total
Dropped 66 nodes (cum <= 29.75ms)
Showing top 10 nodes out of 81
flat flat% sum% cum cum%
1130ms 18.99% 18.99% 3600ms 60.50% LibertyParser/lexer.(*Lexer).NextToken
970ms 16.30% 35.29% 970ms 16.30% LibertyParser/lexer.(*Lexer).readChar
770ms 12.94% 48.24% 770ms 12.94% runtime.kevent
730ms 12.27% 60.50% 730ms 12.27% LibertyParser/lexer.isLetter (inline)
310ms 5.21% 65.71% 1480ms 24.87% LibertyParser/lexer.(*Lexer).readIdentifier
290ms 4.87% 70.59% 290ms 4.87% runtime.madvise
210ms 3.53% 74.12% 210ms 3.53% runtime.pthread_cond_wait
170ms 2.86% 76.97% 170ms 2.86% runtime.memclrNoHeapPointers
140ms 2.35% 79.33% 4200ms 70.59% LibertyParser/lexer.BenchmarkLexer
130ms 2.18% 81.51% 370ms 6.22% LibertyParser/lexer.(*Lexer).readString
注意 flat
代表執行該 func 所需要的時間 (不包含內部其他 func 所需要的時間),而 cum
則是包含全部函示的執行時間。接下來就可以看到整個列表需要改善的項目,像是要改善 readChar
就可以直接執行 list readChar
(pprof) list readChar
Total: 5.95s
ROUTINE ======================== LibertyParser/lexer.(*Lexer).readChar in /Users/appleboy/git/appleboy/LibertyParser/lexer/lexer.go
970ms 970ms (flat, cum) 16.30% of Total
. . 22: l.readChar()
. . 23: return l
. . 24:}
. . 25:
. . 26:func (l *Lexer) readChar() {
260ms 260ms 27: if l.readPosition >= len(l.Data) {
. . 28: // End of input (haven't read anything yet or EOF)
. . 29: // 0 is ASCII code for "NUL" character
. . 30: l.char = 0
. . 31: } else {
620ms 620ms 32: l.char = l.Data[l.readPosition]
. . 33: }
. . 34:
50ms 50ms 35: l.position = l.readPosition
40ms 40ms 36: l.readPosition++
. . 37:}
. . 38:
開發者可以清楚看到每一行所需要的執行時間 (flat, cum),這樣就可以知道時間到底慢在哪邊?哪邊需要進行關鍵性優化。沒有這些數據,開發者就只能自己使用傳統方式 log.Println()
方式來進行除錯。除了上述這些之外,pprof 也提供其他方式來觀看,像是輸出 pdf 之類的,只要在 console 內鍵入 pdf
即可,pdf 內容會有更詳細的圖
除了透過在 console 端操作之外,開發者也可以透過 web 方式來進行 UI 操作,對比 console 來說,看到完整的 pprof 報表,這樣更方便除錯。
go tool pprof -http=:8080 cpu.out
自動會開啟 web 顯示,個人覺得相當的方便,從 console 操作轉到 UI 操作,體驗上還是有差別的。
心得
善用 pprof 可以改善蠻多效能上的問題,也可以抓到哪邊的邏輯寫錯,造成跑太多次,導致效能變差,除了寫法上差異之外,最主要還有程式上的邏輯,也許換個方式效能就改善很多。本篇算是 pprof 的初探,希望大家會喜歡。