使用pprof優化golang性能

點擊上方關注👆👆




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.StartCPUProfilepprof.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
}


我們使用一個額外的 regionInfoslice 來存儲九宮格里的情況,每一次設置數獨中格子的值時,我們更新一下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的幫助下,程序的開銷信息變得一清二楚,優化自然變得輕而易舉。



END


我是小碗湯,我們一起學習。


本文分享自微信公衆號 - 我的小碗湯(mysmallsoup)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章