1 內存優化

1.1 小對象合併成結構體一次分配,減小內存分配次數

作過C/C++的同窗可能知道,小對象在堆上頻繁地申請釋放,會形成內存碎片(有的叫空洞),致使分配大的對象時沒法申請到連續的內存空間,通常建議是採用內存池。Go runtime底層也採用內存池,但每一個span大小爲4k,同時維護一個cache。cache有一個0到n的list數組,list數組的每一個單元掛載的是一個鏈表,鏈表的每一個節點就是一塊可用的內存,同一鏈表中的全部節點內存塊都是大小相等的;可是不一樣鏈表的內存大小是不等的,也就是說list數組的一個單元存儲的是一類固定大小的內存塊,不一樣單元裏存儲的內存塊大小是不等的。這就說明cache緩存的是不一樣類大小的內存對象,固然想申請的內存大小最接近於哪類緩存內存塊時,就分配哪類內存塊。當cache不夠再向spanalloc中分配。html

建議:小對象合併成結構體一次分配,示意以下:c++

for k, v := range m {
    k, v := k, v // copy for capturing by the goroutine
    go func() {
        // using k & v
    }()
}

替換爲:golang

for k, v := range m {
    x := struct {k , v string} {k, v} // copy for capturing by the goroutine
    go func() {
        // using x.k & x.v
    }()
}

 

1.2 緩存區內容一次分配足夠大小空間,並適當複用

在協議編解碼時,須要頻繁地操做[]byte,可使用bytes.Buffer或其它byte緩存區對象。編程

建議:bytes.Buffert等經過預先分配足夠大的內存,避免當Grow時動態申請內存,這樣能夠減小內存分配次數。同時對於byte緩存區對象考慮適當地複用。api

1.3 slice和map採make建立時,預估大小指定容量

slice和map與數組不同,不存在固定空間大小,能夠根據增長元素來動態擴容。數組

slice初始會指定一個數組,當對slice進行append等操做時,當容量不夠時,會自動擴容:緩存

  • 若是新的大小是當前大小2倍以上,則容量增漲爲新的大小;
  • 否而循環如下操做:若是當前容量小於1024,按2倍增長;不然每次按當前容量1/4增漲,直到增漲的容量超過或等新大小。

map的擴容比較複雜,每次擴容會增長到上次容量的2倍。它的結構體中有一個buckets和oldbuckets,用於實現增量擴容:性能優化

  • 正常狀況下,直接使用buckets,oldbuckets爲空;
  • 若是正在擴容,則oldbuckets不爲空,buckets是oldbuckets的2倍,

建議:初始化時預估大小指定容量網絡

m := make(map[string]string, 100)
s := make([]string, 0, 100) // 注意:對於slice make時,第二個參數是初始大小,第三個參數纔是容量

1.4 長調用棧避免申請較多的臨時對象

goroutine的調用棧默認大小是4K(1.7修改成2K),它採用連續棧機制,當棧空間不夠時,Go runtime會不動擴容:多線程

  • 當棧空間不夠時,按2倍增長,原有棧的變量崆直接copy到新的棧空間,變量指針指向新的空間地址;
  • 退棧會釋放棧空間的佔用,GC時發現棧空間佔用不到1/4時,則棧空間減小一半。

好比棧的最終大小2M,則極端狀況下,就會有10次的擴棧操做,這會帶來性能降低。

建議:

  • 控制調用棧和函數的複雜度,不要在一個goroutine作完全部邏輯;
  • 如查的確須要長調用棧,而考慮goroutine池化,避免頻繁建立goroutine帶來棧空間的變化。

1.5 避免頻繁建立臨時對象

Go在GC時會引起stop the world,即整個狀況暫停。雖1.7版本已大幅優化GC性能,1.8甚至量壞狀況下GC爲100us。但暫停時間仍是取決於臨時對象的個數,臨時對象數量越多,暫停時間可能越長,並消耗CPU。

建議:GC優化方式是儘量地減小臨時對象的個數:

  • 儘可能使用局部變量
  • 所多個局部變量合併一個大的結構體或數組,減小掃描對象的次數,一次回儘量多的內存。

2 併發優化

2.1 高併發的任務處理使用goroutine池

goroutine雖輕量,但對於高併發的輕量任務處理,頻繁來建立goroutine來執行,執行效率並不會過高效:

  • 過多的goroutine建立,會影響go runtime對goroutine調度,以及GC消耗;
  • 高並時若出現調用異常阻塞積壓,大量的goroutine短期積壓可能致使程序崩潰。

2.2 避免高併發調用同步系統接口

goroutine的實現,是經過同步來模擬異步操做。在以下操做操做不會阻塞go runtime的線程調度:

  • 網絡IO
  • channel
  • time.sleep
  • 基於底層系統異步調用的Syscall

下面阻塞會建立新的調度線程:

  • 本地IO調用
  • 基於底層系統同步調用的Syscall
  • CGo方式調用C語言動態庫中的調用IO或其它阻塞

網絡IO能夠基於epoll的異步機制(或kqueue等異步機制),但對於一些系統函數並無提供異步機制。例如常見的posix api中,對文件的操做就是同步操做。雖有開源的fileepoll來模擬異步文件操做。但Go的Syscall仍是依賴底層的操做系統的API。系統API沒有異步,Go也作不了異步化處理。

建議:把涉及到同步調用的goroutine,隔離到可控的goroutine中,而不是直接高並的goroutine調用。

2.3 高併發時避免共享對象互斥

傳統多線程編程時,當併發衝突在4~8線程時,性能可能會出現拐點。Go中的推薦是不要經過共享內存來通信,Go建立goroutine很是容易,當大量goroutine共享同一互斥對象時,也會在某一數量的goroutine出在拐點。

建議:goroutine儘可能獨立,無衝突地執行;若goroutine間存在衝突,則能夠採分區來控制goroutine的併發個數,減小同一互斥對象衝突併發數。

3 其它優化

3.1 避免使用CGO或者減小CGO調用次數

GO能夠調用C庫函數,但Go帶有垃圾收集器且Go的棧動態增漲,但這些沒法與C無縫地對接。Go的環境轉入C代碼執行前,必須爲C建立一個新的調用棧,把棧變量賦值給C調用棧,調用結束現拷貝回來。而這個調用開銷也很是大,須要維護Go與C的調用上下文,二者調用棧的映射。相比直接的GO調用棧,單純的調用棧可能有2個甚至3個數量級以上。

建議:儘可能避免使用CGO,沒法避免時,要減小跨CGO的調用次數。

3.2 減小[]byte與string之間轉換,儘可能採用[]byte來字符串處理

GO裏面的string類型是一個不可變類型,不像c++中std:string,能夠直接char*取值轉化,指向同一地址內容;而GO中[]byte與string底層兩個不一樣的結構,他們之間的轉換存在實實在在的值對象拷貝,因此儘可能減小這種沒必要要的轉化

建議:存在字符串拼接等處理,儘可能採用[]byte,例如:

func Prefix(b []byte) []byte {
    return append([]byte("hello", b...))
}

3.3 字符串的拼接優先考慮bytes.Buffer

因爲string類型是一個不可變類型,但拼接會建立新的string。GO中字符串拼接常見有以下幾種方式:

  • string + 操做 :致使屢次對象的分配與值拷貝
  • fmt.Sprintf :會動態解析參數,效率好不哪去
  • strings.Join :內部是[]byte的append
  • bytes.Buffer :能夠預先分配大小,減小對象分配與拷貝

建議:對於高性能要求,優先考慮bytes.Buffer,預先分配大小。非關鍵路徑,視簡潔使用。fmt.Sprintf能夠簡化不一樣類型轉換與拼接。


參考:
1. Go語言內存分配器-FixAlloc
2. https://blog.golang.org/strings