Golang 最佳化之路——臨時對象池

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

寫在前面

在高並發的情況下,如果每次請求都需要申請一塊用於計算的記憶體,比如:

make([]int64, 0, len(ids))

將會是一件成本很高的事情。為了定位項目中的慢語句,我曾經採用“二分法”的方式列印慢日誌,定位程式變慢的代碼位置。它並不是每次都慢,而是每過幾秒鐘就突然變得極其慢,TPS能從2000降到200。引起這個問題就是類似於上面這條語句。

初始化一個slice,初學者會用:

make([]int64, 0)

進階一些的程式員都會知道,這樣第一次分配記憶體相當於沒有分配,如果要後續append元素,會引起slice以指數形式擴充,可以參考下面的代碼,追加了3個元素,slice擴容了3次。

a := make([]int64, 0)fmt.Println(cap(a), len(a))for i := 0; i < 3; i++ {a = append(a, 1)fmt.Println(cap(a), len(a))}0 01 12 24 3

每一次擴容空間,都是會重新申請一塊地區,把就空間裡面的元素複製進來,把新的追加進來。那舊空間裡面的元素怎麼辦?等著記憶體回收唄。

簡單的最佳化方式,就是給自己要用的slice提前申請好空間,類似於最開頭的那行代碼。

make([]int64, 0, len(ids))

這樣做避免了slice多次擴容申請記憶體,但還是有問題的。

堆還是棧?

程式會從作業系統申請一塊記憶體,而這塊記憶體也會被分成堆和棧。棧可以簡單得理解成一次函數調用內部申請到的記憶體,它們會隨著函數的返回把記憶體還給系統。

func F() {temp := make([]int, 0, 20)...}

類似於上面代碼裡面的temp變數,只是內函數內部申請的臨時變數,並不會作為傳回值返回,它就是被編譯器申請到棧裡面。申請到棧記憶體好處:函數返回直接釋放,不會引起記憶體回收,對效能沒有影響。

func F() []int{a := make([]int, 0, 20)return a}

而上面這段代碼,申請的代碼一模一樣,但是申請後作為傳回值返回了,編譯器會認為變數之後還會被使用,當函數返回之後並不會將其記憶體歸還,那麼它就會被申請到堆上面了。申請到堆上面的記憶體才會引起記憶體回收。

那麼考考大家,下面這三種情況怎麼解釋?

func F() {a := make([]int, 0, 20)b := make([]int, 0, 20000)l := 20c := make([]int, 0, l)}

a和b代碼一樣,就是申請的空間不一樣大,但是它們兩個的命運是截然相反的。a前面已經介紹過,會申請到棧上面,而b,由於申請的記憶體較大,編譯器會把這種申請記憶體較大的變數轉移到堆上面。即使是臨時變數,申請過大也會在堆上面申請。

而c,對我們而言其含義和a是一致的,但是編譯器對於這種不定長度的申請方式,也會在堆上面申請,即使申請的長度很短。

可以通過下面的命令查看變數申請的位置。詳細內容可以參考我之前的文章《【譯】最佳化Go的模式》

go build -gcflags='-m' . 2>&1

記憶體片段化

實際項目基本都是通過c := make([]int, 0, l)來申請記憶體,長度都是不確定的。自然而然這些變數都會申請到堆上面了。Golang使用的記憶體回收演算法是『標記——清除』。簡單得說,就是程式要從作業系統申請一塊比較大的記憶體,記憶體分成小塊,通過鏈錶鏈接。每次程式申請記憶體,就從鏈表上面遍曆每一小塊,找到符合的就返回其地址,沒有合適的就從作業系統再申請。如果申請記憶體次數較多,而且申請的大小不固定,就會引起記憶體片段化的問題。申請的堆記憶體並沒有用完,但是使用者申請的記憶體的時候卻沒有合適的空間提供。這樣會遍曆整個鏈表,還會繼續向作業系統申請記憶體。這就能解釋我一開始描述的問題,申請一塊記憶體變成了慢語句。

臨時對象池

如何解決這個問題,首先想到的就是對象池。Golang在sync裡面提供了對象池Pool。一般大家都叫這個為對象池,而我喜歡叫它臨時對象池。因為每次記憶體回收會把池子裡面不被引用的對象回收掉。

func (p *Pool) Get() interface{}

Get selects an arbitrary item from the Pool, removes it from the Pool, and returns it to the caller. Get may choose to ignore the pool and treat it as empty. Callers should not assume any relation between values passed to Put and the values returned by Get.

需要注意的是,Get方法會把返回的對象從池子裡面刪除。所以用完了的對象,還是得重新放回池子。

很快,我寫出了第一版對象池最佳化方案:

var idsPool = sync.Pool{New: func() interface{} {ids := make([]int64, 0, 20000)return &ids},}func NewIds() []int64 {ids := idsPool.Get().(*[]int64)*ids = (*ids)[:0]idsPool.Put(ids)return *ids}

這樣的實現,是把所有slice都放到同一個池子裡面了。為了應對變長的問題,都是按照一個較大的值申請的變數。雖然是一種最佳化,但是使用超大的slice計算,效能並沒有怎麼提升。

緊接著參考了達達大神的代碼sync_pool.go,又寫了一版:

var DEFAULT_SYNC_POOL *SyncPoolfunc NewPool() *SyncPool {DEFAULT_SYNC_POOL = NewSyncPool(5,     30000, 2,     )return DEFAULT_SYNC_POOL}func Alloc(size int) []int64 {return DEFAULT_SYNC_POOL.Alloc(size)}func Free(mem []int64) {DEFAULT_SYNC_POOL.Free(mem)}// SyncPool is a sync.Pool base slab allocation memory pooltype SyncPool struct {classes     []sync.PoolclassesSize []intminSize     intmaxSize     int}func NewSyncPool(minSize, maxSize, factor int) *SyncPool {n := 0for chunkSize := minSize; chunkSize <= maxSize; chunkSize *= factor {n++}pool := &SyncPool{make([]sync.Pool, n),make([]int, n),minSize, maxSize,}n = 0for chunkSize := minSize; chunkSize <= maxSize; chunkSize *= factor {pool.classesSize[n] = chunkSizepool.classes[n].New = func(size int) func() interface{} {return func() interface{} {buf := make([]int64, size)return &buf}}(chunkSize)n++}return pool}func (pool *SyncPool) Alloc(size int) []int64 {if size <= pool.maxSize {for i := 0; i < len(pool.classesSize); i++ {if pool.classesSize[i] >= size {mem := pool.classes[i].Get().(*[]int64)// return (*mem)[:size]return (*mem)[:0]}}}return make([]int64, 0, size)}func (pool *SyncPool) Free(mem []int64) {if size := cap(mem); size <= pool.maxSize {for i := 0; i < len(pool.classesSize); i++ {if pool.classesSize[i] >= size {pool.classes[i].Put(&mem)return}}}}

調用例子:

attrFilters := cache.Alloc(len(ids))defer cache.Free(attrFilters)

重點在Alloc方法。為了能支援變長的slice,這裡申請了多個池子,其大小是從5開始,最大到30000,以2為倍數。也就是5、10、20……

DEFAULT_SYNC_POOL = NewSyncPool(5,     30000, 2,     )
  • 分配記憶體的時候,從池子裡面找滿足容量切最小的池子。比如申請長度是2的,就分配大小為5的那個池子。如果是11,就分配大小是20的那個池子裡面的對象;
  • 如果申請的slice很大,超過了上限30000,這種情況就不使用池子了,直接從記憶體申請;
  • 當然這些參數可以根據自己實際情況調整;
  • 和之前的做法有所區別,把對象重新放回池子是通過Free方法實現的。

結論

為了最佳化介面,前前後後搞了一年。結果還是不錯的,TPS提升了最少30%,TP99也降低很多。

原文連結:Golang 最佳化之路——臨時對象池,轉載請註明來源!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.