如何做Go的性能優化?
Go的性能優化其實總的來說和C/C++等這些都差不多,但也有它自己獨有的排查方法和陷阱,這些都來源於它的語言特性和環境。
1.性能優化前提——任何好的東西都是在正確的前提上
代碼界的很多事是和我們生活的哲學息息相關的,我們想要做好一件事,首先要保證我們能按時完成我們的任務,其次再去想如何把工作做的更好。如果一味只去要求做的盡善盡美可能會導致延期,失敗,半途而廢。
所以,先寫正確的代碼,再去考慮如何去讓代碼更快更好的運行;先完成基本的功能,再去想如何優化它。正確是優化的基礎,沒有這個基礎,任何的優化都是毫無意義的。
2.性能優化限制——架構設計和硬體資源
良好的架構設計是我們能夠發揮性能的前提,一個設計不當的架構付出再多精力優化效果也是大打折扣。這也是我們為什麼經常會看到隨著業務量或者用戶數的增加後天架構會不斷演進變化,如果說一開始設計的架構可以一直支撐下去,那麼請大神請收下我的膝蓋!
硬體資源更好理解,一個16核,64G內存的伺服器和4核,4G內存的垃圾機器對比簡直是天與地。毋庸多說。
3.什麼時候做性能優化
We should forget about small efficiencies, say about 97% of the time; premature optimization is the root of all evil(大概97%的時間,我們應該忘記小的優化, 過早優化是所有邪惡的根源). —— Donald E.Knuth
這句話不是說不去優化,不去思考演算法,而是在早期我們應該更加專註於程序的實現,而不是一開始就去想著優化,你大可以放開去寫。慢慢的會有驅動力讓我們不自覺去優化。
正常情況下這種驅動我覺得有兩種,一種是自我驅動,比如經歷過搞過ACM或者演算法競賽的童鞋們在面對一個問題時會不自覺地從複雜度角度分析問題;或者一個「強迫症患者」不能忍受慢,卡,崩等等情況。
另一種是環境驅動,比如高並發環境,高精度環境,低延遲環境,大數據環境等等對於我們系統的某一方面甚至多個方面都有苛刻的要求,逼這著我們需要不斷優(jia)化(ban),優(jia)化(ban),再優(jia)化(ban)。
- 當你意識到這個函數可能會被經常調用,就需要想辦法的優化
- 當你意識到這個數據結構設計不合理導致內存佔用過高,就需要想辦法優化
- 當用戶反映服務響應太慢,就需要優化
- 當老闆既要好的服務又不想再花錢買機器,就需要優化
- 當代碼太亂,問題百出,經常報警告打擾和女票玩耍,就需要優化
- 。。。
4.花多長時間來做性能優化
有人說是二八定律,又名80/20定律、帕累托法則(定律)也叫巴萊特定律、最省力的法則、不平衡原則等,被廣泛應用於社會學及企業管理學等。是19世紀末20世紀初義大利經濟學家巴萊多發現的。他認為,在任何一組東西中,最重要的只佔其中一小部分,約20%,其餘80%儘管是多數,卻是次要的,因此又稱二八定律。—— 百度百科
我覺得雖然我們不必一定按照二比八的要求去執行,但毫無疑問的是優化會耗費我們非常多的時間和精力,並且遠遠大於我們系統實現的時間,或者說自從第一次開發完,以後所有的時間都是在做優化。自己的曾經的經歷,當時為了給某銀行做60W終端測試優化一個API緩存系統,基本功能實現兩周就完成了,後面我和性能QA童鞋一波波優化——測試——優化——測試,花費了一個多月時間做這件事,這還沒完,後面在真實環境測試過程仍然暴露了很多問題,例如goruntine暴增積壓,CPU暴增等等,後來發現是架構設計和組件使用上的問題,是的,當出現這樣的問題時不是不可以解決,但是為了解決這樣的問題會把系統搞的複雜,臃腫,雖然開發經驗不多,但我覺得該是代碼實現的代碼實現,該是組件解決的問題就應該組件來解決,架構設計問題就是架構需要改進,不要說都可以在代碼中解決,除非是不得已。
5.工欲善其事,必先利其器
首先是代碼層次,好的代碼是性能的關鍵因素,實現函數效率怎麼樣,排序是不是高效,操作並發性高不高等等,你可以使用代碼質量評估工具來做評估,當然最好還是讓有經驗的司機們手把手指導。
Go代碼評估工具:
- goreporter – 生成Go代碼質量評估報告n
- dingo-hunter – 用於在Go程序中找出deadlocks的靜態分析器n
- flen – 在Go程序包中獲取函數長度信息n
- go/ast – Package ast聲明了關於Go程序包用於表示語法樹的類型n
- gocyclo – 在Go源代碼中測算cyclomatic函數複雜性n
- Go Meta Linter – 同時Go lint工具且工具的輸出標準化n
- go vet – 檢測Go源代碼並報告可疑的構造n
- ineffassign – 在Go代碼中檢測無效賦值n
- safesql – Golang靜態分析工具,防止SQL注入n
然後是如何在運行過程來調試Go程序,Go自帶了一個pprof工具,這個工具可以做CPU和內存的profiling。使用可以參考之前介紹文章:一個內部API系統的性能優化 - 知乎專欄
package mainnimportn (n "log"n "net/http"n _"net/http/pprof"n )nfunc main() {n go func() {n //port is you coustom define.n log.Println(http.ListenAndServe("localhost:7000", nil))n }()n //do your stuff.n}n
只需要引入net/http 和 _"net/http/pprof"即可,然後配合工具生成流程圖,佔比圖清晰明了。
或者對於一些程序你還可以在運行時去改變它,調試它,使用google/gops 谷歌出品,你可以去查看棧,內存,CPU,heap等等信息,很不錯,但是我不喜歡它開啟了服務埠,這個項目剛開始是不需要使用新的埠,直接使用套接字文件通信,但是因為無法在windows上實現,最後作罷,從此好感降低了!
當然你還可以使用GDB工具,最新的GDB貌似還加入了查看goruntine的命令,很棒!
6.演算法與優化思路
這個不用多說,說實話個人覺得演算法是區分工程師和碼農的一個很大分界點,演算法可以說是基本能力,很多人不以為然覺得只是面試門檻,但是看看代碼實現中數據結構的設計和演算法實現就明白了!當遇到問題會不自覺的想到一個演算法,這個目的就夠了,其實並沒有說演算法非常牛逼,其實之前老司機聊天也說過只要你能在遇到問題能夠想到用什麼演算法解決即可。
各種排序,集合操作,查詢等等,沒有最好的演算法,只要最適合的演算法。
至於哪些方面需要優化,一方面是演算法的效率還要就是現象,例如CPU特別高那麼看看goruntine的調度,哪個函數佔用比高,是不是存在死循環;內存大,看看是不是有大的內存分配沒有及時回收,或者是不是有頻繁的內存分配,是不是有內存泄露?響應慢是卡在哪裡,是執行效率還是和組件通信等等。
7.Go的陷阱與技巧
a.make的陷阱
func main() {nts := make([]int, 3)nts = append(s, 1, 2, 3)ntfmt.Println(s)n}n結果n[0 0 0 1 2 3]n
b.map讀寫衝突,產生競態
c.文件打開,資料庫連接記得一定要關閉或釋放,一般使用defer
d.對於一個struct值的map,你無法更新單個的struct值
e.簡化range
for range m {n}n
f.defer的陷阱
有名返回值則是在函數聲明的同時就已經被聲明,匿名返回值是在return執行時被聲明,所以在defer語句中只能訪問有名返回值,而不能直接訪問匿名返回值。
package mainnnimport (nt"fmt"n)nnfunc main() {ntfmt.Println("return:", defer_call())n}nnfunc defer_call() int {ntvar i intntdefer func() {ntti++nttfmt.Println("defer1:", i)nt}()ntdefer func() {ntti++nttfmt.Println("defer2:", i)nt}()ntreturn in}nndefer2: 1ndefer1: 2nreturn: 0n
Q2.
package mainnnimport (nt"fmt"n)nnfunc main() {ntfmt.Println("return:", defer_call())n}nnfunc defer_call() (i int) {ntdefer func() {ntti++nttfmt.Println("defer1:", i)nt}()ntdefer func() {ntti++nttfmt.Println("defer2:", i)nt}()ntreturn in}nndefer2: 1ndefer1: 2nreturn: 2n
g.短式變數聲明的陷阱
那些使用過動態語言的開發者而言對於短式變數聲明的語法很熟悉,所以很容易讓人把它當成一個正常的分配操作。這個錯誤,將不會出現編譯錯誤,但將不會達到你預期的效果。
package mainnimport "fmt"nfunc main() { n value := 1n fmt.Println(value) // prints 1n {n fmt.Println(value) // prints 1n value := 2n fmt.Println(value) // prints 2n }n fmt.Println(value) // prints 1 (bad if you need 2)n}n
這個說到底是代碼邊界和變數影響範圍問題。
h.nil和顯式類型
nil標誌符用於表示interface、函數、maps、slices和channels的「零值」。如果你不指定變數的類型,編譯器將無法編譯你的代碼,因為它不知道具體的類型,同時你也不能給string賦nil值。nn
package mainnnfunc main() {n var value1 = nil // errorn _ = value1n var value2 string = nil // errorn if value2 == nil { // errorn value2 = "test"n }n}n
應該
package mainnnfunc main() {ntvar value1 interface{} = nil // errornt_ = value1ntvar value2 stringntif value2 == "" {nttvalue2 = "test"nt}n}n
i.全部是值傳遞,沒有引用傳遞
如果你是一個C或則C++開發者,那麼知道數組就是指針。當你向函數中傳遞數組時,函數會參照相同的內存區域,這樣它們就可以修改原始的數據。但Go中的數組是數值,因此當你向函數中傳遞數組時,函數會得到原始數組數據的一份複製。如果你打算更新數組的數據,你將會失敗。nn
j.select下的所有case遍歷是隨機的,在使用的過程中要注意,這和switch是不同的
l.使用介面實現一個類型分類函數:
func classifier(items ...interface{}) {n for i, x := range items {n switch x.(type) {n case bool:n fmt.Printf("param #%d is a booln", i)n case float64:n fmt.Printf("param #%d is a float64n", i)n case int, int64:n fmt.Printf("param #%d is an intn", i)n case nil:n fmt.Printf("param #%d is niln", i)n case string:n fmt.Printf("param #%d is a stringn", i)n default:n fmt.Printf("param #%d』s type is unknownn", i)n }n }n}n
l.Map值在獲取的時候是無序的,所以當我們需要有序時就需要通過字元串數組排序間接得到
package mainnnimport (n "fmt"n "sort"n)nnfunc main() {n var m = map[string]int{n "unix": 0,n "python": 1,n "go": 2,n "javascript": 3,n "testing": 4,n "philosophy": 5,n "startups": 6,n "productivity": 7,n "hn": 8,n "reddit": 9,n "C++": 10,n }n var keys []stringn for k := range m {n keys = append(keys, k)n }n sort.Strings(keys)n for _, k := range keys {n fmt.Println("Key:", k, "Value:", m[k])n }n}n
m.init函數
開發過程我們經常會遇到主要邏輯開始前要聲明或者一些全局的變數或者初始化操作,或者有時候我們僅僅需要import一些包,並不需要使用裡面的函數,那就需要使用init初始化函數,一個package中可以有多個init,比如你在demo/A.go,demo/B.go都有一個init那麼它們都會執行。
n.Go程序顯示佔用內存有時候並不是真正在用的內存,只是還沒還給操作系統
o.雨痕老師的研究
雨痕學堂 - SegmentFault
p.Golang的五十度灰
中文版:Go的50度灰:Golang新開發者要注意的陷阱和常見錯誤
英文版:50 Shades of Go: Traps, Gotchas, and Common Mistakes for New Golang Devs
後續再補充。。。
推薦閱讀:
※我們用50次遊戲性能的深度優化,總結出了五條「毒雞湯」
※【求知探新】《為誰而煉金》UI界面載入性能分析
※多個提高Node.js應用吞吐量的小優化技巧介紹
※【譯】針對 Airbnb 清單頁的 React 性能優化
※記一次冷雨寒風中的UWA優化日(內附技術PPT)
TAG:性能优化 | Go语言 | golang最佳实践 |