golang gc優化總結 原

go語言的GC

使用的內存回收機制

go語言垃圾回收總體採用的是經典的mark and sweep(標記-清除)算法。

該算法法分爲兩步:

  1. 標記從根變量開始迭代得遍歷所有被引用的對象,對能夠通過應用遍歷訪問到的對象都進行標記爲“被引用”;
  2. 清除是在標記完成後進行的操作,對沒有標記過的內存進行回收(回收同時可能伴有碎片整理操作)。

這種方法解決了引用計數的不足,但是也有比較明顯的問題:

每次啓動垃圾回收都會暫停當前所有的正常代碼執行,回收是系統響應能力大大降低!當然後續也出現了很多mark&sweep算法的變種(如 三色標記法 )優化了這個問題。

如何工作?

代碼存在於棧中,對象分配在堆中,當程序運行到一定時間的時候:

  1. 標記(mark phase):gc會暫停正在運行的程序(提高gc的優先級,搶佔cpu,1.5之後是並行了),這時候,gc對所有已分配對象進行遍歷,並標記處在棧中已經被引用的對象。
  2. 回收:掃描完這些對象之後,將沒有被引用(reference)的對象進行回收(釋放內存),並清理gc自己的對象庫。

和JAVA之類的語言比起來,golang 中的垃圾回收模型還是相對簡單的。

我畫了個草圖描述一下流程:

版本更迭

  1. GO1.5引入併發GC後,runtime會對一個goroutine在上次掃描過stack後是否執行過,進行了跟蹤。STW階段會檢查每個goroutine是否執行過,然後會重新掃描那些執行過的。在GO1.7開始,runtime會維護一個獨立的短list,這樣就不需要在STW期間再遍歷一次所有的goroutine,同時極大的減少了那些會觸發kernel的NUMA遷移的內存訪問。

  2. 1.7中,amd64的編譯器會默認維護frame pointers,這樣標準的debug和性能測試工具,例如perf,就可以debug當前的Go函數調用堆棧 了。

  3. 1.8中,由於消除了GC的“stop-the-world stack re-scanning”,使得GC STW(stop-the-world)的時間通常低於100微秒,甚至經常低於10微秒。當然這或多或少是以犧牲“吞吐”作爲代價的。

  4. 1.9中,用於觸發垃圾收集的庫函數現在可觸發併發垃圾收集,並在吞吐和低延遲上做了一個的平衡。

    具體來說,runtime.GC,debug.SetGCPercent和debug.FreeOSMemory,可觸發併發垃圾回收,阻止調用goroutine,直到垃圾收集完成。

    此外,如果由於新的GOGC值的需要,debug.SetGCPercent函數可以僅觸發垃圾回收,這使得可以即時調整GOGC。在使用包含許多對象的大型(> 50GB)堆的應用程序中,對象的分配性能顯着提高。runtime.ReadMemStats函數即使對於非常大的堆也少於100μs。

硬件參數調優

涉及算法的問題,總是會有些參數。GO gc參數主要控制的是下一次gc開始的時候的內存使用量。

比如當前的程序使用了4M的對內存(這裏說的是堆內存),即是說程序當前reachable的內存爲4m,當程序佔用的內存達到reachable*(1+GO gc/100)=8M的時候,gc就會被觸發,開始進行相關的gc操作。

如何對GO gc的參數進行設置,要根據生產情況中的實際場景來定,比如GO gc參數提升,來減少gc的頻率。

代碼規範(gopher 大會)

減少對象

減少對象分配:所謂減少對象的分配,實際上是儘量做到,對象的重用。

比如像如下的兩個函數定義:

func(r*Reader)Read()([]byte,error)
//此函數沒有形參,每次調用的時候返回一個[]byte,第二個函數在每次調用的時候,形參是一個buf []byte 類型的對象,之後返回讀入的byte的數目。
func(r*Reader)Read(buf[]byte)(int,error)
//此函數在每次調用的時候都會分配一段空間,這會給gc造成額外的壓力。第二個函數在每次迪調用的時候,會重用形參聲明。

string與[]byte轉化

在stirng與[]byte之間進行轉換,會給gc造成壓力 通過gdb,可以先對比下兩者的數據結構:

type = struct []uint8 {    uint8 *array;    int len;    int cap;}
type = struct string {    uint8 *str;    int len;}

兩者發生轉換的時候,底層數據結結構會進行復制,因此導致gc效率會變低。

解決策略:

  1. 一直使用[]byte,特別是在數據傳輸方面,[]byte中也包含着許多string會常用到的有效的操作。
  2. 使用更爲底層的操作直接進行轉化,避免複製行爲的發生(主要是使用unsafe.Pointer直接進行轉化。參考資料-雨痕血糖)。

字符串拼接

遵循策略:

儘量減少使用+對字符串進行拼接,由於採用+來進行string的連接會生成新的對象,降低gc的效率,好的方式是通過append函數來進行。

但是要注意如下問題:

b := make([]int, 1024)
b = append(b, 99)
fmt.Println("len:", len(b), "cap:", cap(b))

在使用了append操作之後,數組的空間由1024增長到了1312。

所以如果能提前知道數組的長度的話,最好在最初分配空間的時候就做好空間規劃操作,會增加一些代碼管理的成本,同時也會降低gc的壓力,提升代碼的效率。

總結

作爲一個code來說,要重視自己的代碼性能,減少內存分配和提高對象重用尤爲重要。

特別是在摩爾定律即將失效的年代,性能又開始重新被提上的議程。

ps:

摩爾定律是由英特爾(Intel)創始人之一戈登·摩爾(Gordon Moore)提出來的。其內容爲:當價格不變時,集成電路上可容納的元器件的數目,約每隔18-24個月便會增加一倍,性能也將提升一倍。換言之,每一美元所能買到的電腦性能,將每隔18-24個月翻一倍以上。這一定律揭示了信息技術進步的速度。

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