點擊上方關注👆👆
Donald E.Knuth說過一句非常著名的話,過早的優化是萬惡之源。原文如下:
We should forget about small efficiencies, say about 97% of the time; premature optimization is the root of all evil.
我是十分贊同這句話的,並且在開發過程中也深有體會。什麼叫做過早的優化呢?即不需要考慮優化的時候你在考慮優化。這絕對不意味着可以任性地寫代碼,隨意地選擇數據結構和算法。這句話是告訴我們,在程序開發的早期階段,程序員應該專注在程序的邏輯實現上,而不是專注在程序的性能優化上。用正確的數據結構和算法,優美合理的語句實現你要的功能。而不是滿腦子在想:“這個函數是不是可以優化一下?”。
我們都知道,性能最好的代碼往往並不是優美直觀的代碼,往往看起來非常晦澀。下圖是 JS 轉換字符串到數字的三個方法在 Chrome 下的性能對比。可以看出, +
是最快的方法。但是 +str
這種寫法明顯是不如 parseInt(str)
或者是 Number(str)
容易理解。Donald E.Knuth 的那句話,我的理解就是在提醒我們,不用使用+str
,而應該使用更加語義化的 parseInt(str)
。
不應該過早的優化,那麼應該做的就是在適當的時候進行優化。程序在功能開發完畢並且測試好以後,就可以進入優化環節了。所有的優化都應該基於性能分析(Profiling),憑空想象進行優化是一件很危險並且沒有效率的事情。很多你覺得可以優化的點說不定編譯器早替你做了,很多你覺得很慢的地方說不定非常快。
Golang提供了非常棒的Profiling工具,可以很容易地得到CPU和內存的Profiling數據。更加讚的是,Golang還提供了工具來可視化這些數據,一眼就可以看出程序的性能瓶頸在哪兒。調優從未如此輕鬆。
go中有pprof包來做代碼的性能監控,分別有包:
net/http/pprof
runtime/pprof
其實net/http/pprof中只是使用runtime/pprof包來進行封裝了一下,並在http端口上暴露出來。
Package runtime/pprof
如果你的go程序只是一個應用程序,比如計算 fabonacci 數列,我們就需要使用到runtime/pprof,具體做法就是用到 pprof.StartCPUProfile 和pprof.StopCPUProfile。比如下面的例子:
func main() {
//create prof file
f, err := os.Create("cpu-profile.prof")
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
//... this is program you want to profile
pprof.StopCPUProfile()
}
程序運行後,pprof會將Profiling數據寫到指定的文件當中,然後通過 go tool pprof
就可以查看。
我們來Profiling一個簡單的Fibonacci程序。
package main
import (
"fmt"
"log"
"os"
"runtime/pprof"
)
func main() {
f, err := os.Create("cpu-profile.prof")
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
fmt.Println(fibonacci(45))
pprof.StopCPUProfile()
}
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
編譯以後,運行程序便可以生成cpu-profile.prof
文件。使用
go tool pprof finabocci cpu-profile.prof
進入Profiling控制檯,輸入web
(需要安裝graphviz)
Graphviz 下載地址:
http://down2.opdown.com:8181/opdown/graphviz.zip
Package net/http/pprof
如果你的go程序是用http包啓動的web服務器,你想查看自己的web服務器的狀態。這個時候就可以選擇net/http/pprof。你只需要引入包_"net/http/pprof",然後就可以在瀏覽器中使用http://localhost:port/debug/pprof/直接看到當前web服務的狀態,包括CPU佔用情況和內存使用情況等。具體使用情況你可以看godoc的說明。
如果你的go程序不是web服務器,而是一個服務進程,那麼你也可以選擇使用net/http/pprof包,同樣引入包net/http/pprof,然後在開啓另外一個goroutine來開啓端口監聽。
比如:
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
運行程序後,我們可以通過下面的命令採集30s的數據並生成SVG調用圖:
go tool pprof -web http://10.75.25.126:9091/debug/pprof/profile
Benchmark Test
每一次都手動引入pprof
包比較麻煩,也沒有必要。一般Golang的性能測試我們會使用Golang提供的Benchmark功能,Golang提供了命令行參數我們可以直接得到測試文件中Benchmark的Profiling數據。不需要添加任何代碼。
下面我們來寫一個Benchmark測試一下Golang的標準庫函數rand.Intn
的性能如何。
package main
import (
"math/rand"
"testing"
)
func BenchmarkRandom(b *testing.B) {
for i := 0; i < b.N; i++ {
random()
}
}
func random() int {
return rand.Intn(100)
}
因爲pprof需要編譯好的二進制文件以及prof文件一起纔可以分析。所以先要編譯這一段測試程序。
$ go test -c go_test.go
$ ./main.test -test.bench=. -test.cpuprofile=cpu-profile.prof
testing: warning: no tests to run
BenchmarkRandom-8 50000000 30.5 ns/op
可以看出Go標準庫的 rand.Intn
性能很好。測試運行完畢以後,我們也得到了相應的CPU Profiling數據。使用go tool pprof
打開以後,使用top 5
指令得到開銷排名前五的函數。五個裏面有兩個是sync/atomic
包的函數,保證了原子操作。很明顯,rant.Intn
是併發安全的。
$ go tool pprof main.test cpu-profile.prof
(pprof) top 5
780ms of 1370ms total (56.93%)
Showing top 5 nodes out of 35 (cum >= 610ms)
flat flat% sum% cum cum%
270ms 19.71% 19.71% 270ms 19.71% runtime.usleep
170ms 12.41% 32.12% 840ms 61.31% math/rand.(*Rand).Int31n
150ms 10.95% 43.07% 150ms 10.95% sync/atomic.AddUint32
110ms 8.03% 51.09% 110ms 8.03% sync/atomic.CompareAndSwapUint32
80ms 5.84% 56.93% 610ms 44.53% math/rand.(*Rand).Int63
Example Sudoku
下面我用Godoku這個項目爲例,看看怎麼具體優化一個程序。Godoku是一個Go編寫的暴力破解數獨的程序,邏輯比較簡單,從上到下從左到右掃描每一個空格,從1到9開始填寫數字,一旦數字無效(行衝突,列衝突或者9宮格衝突),那麼就換一個數字,如果所有數字都換了還無效,那麼就退回上一個格子。繼續這個過程。
Step1
程序自帶了測試和Benchmark,所以我們先來生成一個Profiling文件,看看哪個地方開銷最大。
很明顯,ValidInSquare
這個函數開銷很大。這個函數是檢測一個數字在九宮格里面存不存在。作者的實現如下。
func (s *Sudoku) ValidInSquare(row, col, val int) bool {
row, col = int(row/3)*3, int(col/3)*3
for i := row; i < row+3; i++ {
for j := col; j < col+3; j++ {
//fmt.Printf("row, col = %v, %v
", i, j)
if s.board[i][j] == val {
return false
}
}
}
return true
}
循環判斷有沒有這個數。邏輯很簡單,但是Profiling告訴我們,這裏成了性能瓶頸。每一次測試數字都要調用這個方法,而這個方法內部是一個循環,調用如此頻繁的方法採用循環肯定是不行的。
Step2
這裏我們採用經典的空間換時間思路,使用另外一個結構存儲九宮格內的狀態信息,使得查詢一個數字在九宮格內有沒有可以通過簡單的數組訪問得到。
s.regionInfo = make([]int, s.dim * s.dim / 9)
func (s *Sudoku) updateRegion(row, col, val, delta int) {
region := (row/3)*3 + col/3
key := region*9 + val - 1
s.regionInfo[key] += delta
}
func (s *Sudoku) checkRegion(row, col, val int) bool {
region := (row/3)*3 + col/3
key := region*9 + val - 1
return s.regionInfo[key] == 1
}
我們使用一個額外的 regionInfo
slice 來存儲九宮格里的情況,每一次設置數獨中格子的值時,我們更新一下regionInfo的信息。當要檢查某個數在某個九宮格中是否已經存在時,直接查詢regionInfo即可。
func (s *Sudoku) ValidInSquare(row, col, val int) bool {
return !s.checkRegion(row, col, val)
}
再運行一次測試,看看性能改善了多少。
很好!CPU開銷已經由9770ms降低到了5460ms,性能提高79%。現在程序的性能瓶頸已經是ValidInColumnAndRow
這個函數了。
Step3
作者 ValidInColumnAndRow
函數的實現仍然是直觀簡單的循環。
func (s *Sudoku) ValidInColumnAndRow(row, col, val int) bool {
for i := 0; i < 9; i++ {
if s.board[row][i] == val ||
s.board[i][col] == val {
return false
}
}
return true
}
我們使用同樣的策略來優化ValidInColumnAndRow
這個函數,使用額外的數據結構存儲每一行和每一列的數字狀態信息。這樣查詢時可以馬上返回,而不需要做任何循環比較。
func (s *Sudoku) updateRowAndCol(row, col, val, delta int) {
rowKey := row*9 + val - 1
colKey := col*9 + val - 1
s.rowInfo[rowKey] += delta
s.colInfo[colKey] += delta
}
func (s *Sudoku) checkRowOrCol(row, col, val int) bool {
rowKey := row*9 + val - 1
colKey := col*9 + val - 1
return s.rowInfo[rowKey] == 1 || s.colInfo[colKey] == 1
}
func (s *Sudoku) ValidInColumnAndRow(row, col, val int) bool {
return !s.checkRowOrCol(row, col, val)
}
我們再來看看Profiling數據。
性能再次得到了提升,由5460ms降低到了3610ms。初步看來,已經沒有了明顯可以優化的地方了。到此爲止,我們的程序性能已經得到了170%的提升!我們並沒有怎麼努力,只不過是生成了Profiling文件,一眼看出問題在哪兒,然後針對性的優化而已。
感謝Golang提供了這套超讚的pprof工具,性能調優變得如此輕鬆和愉悅。這裏我所舉的只是pprof功能的冰山一角,pprof的強大功能遠不止這些。比如可以使用list
指令查看函數的源碼中每一行代碼的開銷以及使用weblist
指令查看函數彙編以後每一句彙編指令的開銷等等。不僅是CPU Profiling,pprof同樣支持Memory Profiling,可以幫助你檢查程序中內存的分配情況。總之,在pprof的幫助下,程序的開銷信息變得一清二楚,優化自然變得輕而易舉。
我是小碗湯,我們一起學習。
本文分享自微信公衆號 - 我的小碗湯(mysmallsoup)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。