1. 程式人生 > >如何做Go的效能優化?(轉)

如何做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程式碼質量評估報告
  • dingo-hunter – 用於在Go程式中找出deadlocks的靜態分析器
  • flen – 在Go程式包中獲取函式長度資訊
  • go/ast – Package ast聲明瞭關於Go程式包用於表示語法樹的型別
  • gocyclo – 在Go原始碼中測算cyclomatic函式複雜性
  • Go Meta Linter – 同時Go lint工具且工具的輸出標準化
  • go vet – 檢測Go原始碼並報告可疑的構造
  • ineffassign – 在Go程式碼中檢測無效賦值
  • safesql – Golang靜態分析工具,防止SQL注入

 

然後是如何在執行過程來除錯Go程式,Go自帶了一個pprof工具,這個工具可以做CPU和記憶體的profiling。使用可以參考之前介紹文章:一個內部API系統的效能優化 - 知乎專欄

package main
import
   (
    "log"
    "net/http" _"net/http/pprof" ) func main() { go func() { //port is you coustom define. log.Println(http.ListenAndServe("localhost:7000", nil)) }() //do your stuff. } 

只需要引入net/http 和 _"net/http/pprof"即可,然後配合工具生成流程圖,佔比圖清晰明瞭。

或者對於一些程式你還可以在執行時去改變它,除錯它,使用google/gops 谷歌出品,你可以去檢視棧,記憶體,CPU,heap等等資訊,很不錯,但是我不喜歡它開啟了服務埠,這個專案剛開始是不需要使用新的埠,直接使用套接字檔案通訊,但是因為無法在windows上實現,最後作罷,從此好感降低了!

當然你還可以使用GDB工具,最新的GDB貌似還加入了檢視goruntine的命令,很棒!

 

6.演算法與優化思路

這個不用多說,說實話個人覺得演算法是區分工程師和碼農的一個很大分界點,演算法可以說是基本能力,很多人不以為然覺得只是面試門檻,但是看看程式碼實現中資料結構的設計和演算法實現就明白了!當遇到問題會不自覺的想到一個演算法,這個目的就夠了,其實並沒有說演算法非常牛逼,其實之前老司機聊天也說過只要你能在遇到問題能夠想到用什麼演算法解決即可。

各種排序,集合操作,查詢等等,沒有最好的演算法,只要最適合的演算法

至於哪些方面需要優化,一方面是演算法的效率還要就是現象,例如CPU特別高那麼看看goruntine的排程,哪個函式佔用比高,是不是存在死迴圈;記憶體大,看看是不是有大的記憶體分配沒有及時回收,或者是不是有頻繁的記憶體分配,是不是有記憶體洩露?響應慢是卡在哪裡,是執行效率還是和元件通訊等等。

 

7.Go的陷阱與技巧

a.make的陷阱

func main() { s := make([]int, 3) s = append(s, 1, 2, 3) fmt.Println(s) } 結果 [0 0 0 1 2 3] 

b.map讀寫衝突,產生競態

c.檔案開啟,資料庫連線記得一定要關閉或釋放,一般使用defer

d.對於一個struct值的map,你無法更新單個的struct值

 

e.簡化range

for range m {
}

f.defer的陷阱

有名返回值則是在函式宣告的同時就已經被宣告,匿名返回值是在return執行時被宣告,所以在defer語句中只能訪問有名返回值,而不能直接訪問匿名返回值。

package main

import (
	"fmt"
) func main() { fmt.Println("return:", defer_call()) } func defer_call() int { var i int defer func() { i++ fmt.Println("defer1:", i) }() defer func() { i++ fmt.Println("defer2:", i) }() return i } defer2: 1 defer1: 2 return: 0 

Q2.

package main

import (
	"fmt"
) func main() { fmt.Println("return:", defer_call()) } func defer_call() (i int) { defer func() { i++ fmt.Println("defer1:", i) }() defer func() { i++ fmt.Println("defer2:", i) }() return i } defer2: 1 defer1: 2 return: 2 

g.短式變數宣告的陷阱

那些使用過動態語言的開發者而言對於短式變數宣告的語法很熟悉,所以很容易讓人把它當成一個正常的分配操作。這個錯誤,將不會出現編譯錯誤,但將不會達到你預期的效果。

package main
import "fmt"
func main() { value := 1 fmt.Println(value) // prints 1 { fmt.Println(value) // prints 1 value := 2 fmt.Println(value) // prints 2 } fmt.Println(value) // prints 1 (bad if you need 2) } 

這個說到底是程式碼邊界和變數影響範圍問題。

 

h.nil和顯式型別

nil標誌符用於表示interface、函式、maps、slices和channels的“零值”。如果你不指定變數的型別,編譯器將無法編譯你的程式碼,因為它不知道具體的型別,同時你也不能給string賦nil值。

package main

func main() { var value1 = nil // error _ = value1 var value2 string = nil // error if value2 == nil { // error value2 = "test" } } 

應該

package main

func main() { var value1 interface{} = nil // error _ = value1 var value2 string if value2 == "" { value2 = "test" } } 

i.全部是值傳遞,沒有引用傳遞

如果你是一個C或則C++開發者,那麼知道陣列就是指標。當你向函式中傳遞陣列時,函式會參照相同的記憶體區域,這樣它們就可以修改原始的資料。但Go中的陣列是數值,因此當你向函式中傳遞陣列時,函式會得到原始陣列資料的一份複製。如果你打算更新陣列的資料,你將會失敗。

 

j.select下的所有case遍歷是隨機的,在使用的過程中要注意,這和switch是不同的

l.使用介面實現一個型別分類函式:

 

func classifier(items ...interface{}) { for i, x := range items { switch x.(type) { case bool: fmt.Printf("param #%d is a bool\n", i) case float64: fmt.Printf("param #%d is a float64\n", i) case int, int64: fmt.Printf("param #%d is an int\n", i) case nil: fmt.Printf("param #%d is nil\n", i) case string: fmt.Printf("param #%d is a string\n", i) default: fmt.Printf("param #%d’s type is unknown\n", i) } } } 

 

l.Map值在獲取的時候是無序的,所以當我們需要有序時就需要通過字串陣列排序間接得到

package main

import (
    "fmt"
    "sort" ) func main() { var m = map[string]int{ "unix": 0, "python": 1, "go": 2, "javascript": 3, "testing": 4, "philosophy": 5, "startups": 6, "productivity": 7, "hn": 8, "reddit": 9, "C++": 10, } var keys []string for k := range m { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { fmt.Println("Key:", k, "Value:", m[k]) } } 

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