GO效能最佳化小結

來源:互聯網
上載者:User

文章目錄

  • 1 記憶體最佳化
    • 1.1 小對象合并成結構體一次分配,減少記憶體配置次數
    • 1.2 緩衝區內容一次分配足夠大小空間,並適當複用
    • 1.3 slice和map采make建立時,預估大小指定容量
    • 1.4 長調用棧避免申請較多的臨時對象
    • 1.5 避免頻繁建立臨時對象
  • 2 並發最佳化
    • 2.1 高並發的任務處理使用goroutine池
    • 2.2 避免高並發調用同步系統介面
    • 2.3 高並發時避免共用對象互斥
  • 3 其它最佳化
    • 3.1 避免使用CGO或者減少CGO調用次數
    • 3.2 減少[]byte與string之間轉換,盡量採用[]byte來字串處理
    • 3.3 字串的拼接優先考慮bytes.Buffer

1 記憶體最佳化

1.1 小對象合并成結構體一次分配,減少記憶體配置次數

做過C/C++的同學可能知道,小對象在堆上頻繁地申請釋放,會造成記憶體片段(有的叫空洞),導致分配大的對象時無法申請到連續的記憶體空間,一般建議是採用記憶體池。Go runtime底層也採用記憶體池,但每個span大小為4k,同時維護一個cache。cache有一個0到n的list數組,list數組的每個單元掛載的是一個鏈表,鏈表的每個節點就是一塊可用的記憶體,同一鏈表中的所有節點記憶體塊都是大小相等的;但是不同鏈表的記憶體大小是不等的,也就是說list數組的一個單中繼存放區的是一類固定大小的記憶體塊,不同單元裡儲存的記憶體塊大小是不等的。這就說明cache緩衝的是不同類大小的記憶體對象,當然想申請的記憶體大小最接近於哪類緩衝記憶體塊時,就分配哪類記憶體塊。當cache不夠再向spanalloc中分配。

建議:小對象合并成結構體一次分配,示意如下:

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

替換為:

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 }()}
123456 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緩衝區對象考慮適當地複用。

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時,第二個參數是初始大小,第三個參數才是容量
12 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最佳化方式是儘可能地減少臨時對象的個數:

  • 盡量使用局部變數(棧上分配)
  • 多個局部變數合并一個大的結構體或數組(類似於1.1),減少掃描對象的次數,一次回儘可能多的記憶體。

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出在拐點。

建議:

1)、goroutine盡量獨立,無衝突地執行;若goroutine間存在衝突,則可以采分區來控制goroutine的並發個數,減少同一互斥對象衝突並發數。

2)、採用分區,將需要互斥保護的資料,分成多個固定分區(建議是2的整數倍,如256),訪問時先定位分區(不互斥),這樣就可降低多個Go程競爭1個資料分區的機率。

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...))}
123 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可以簡化不同類型轉換與拼接。

 

 

 

 

 

參考連結:

https://www.cnblogs.com/zhangboyu/p/7456609.html

https://zhuanlan.zhihu.com/p/21514693

 

 

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.