日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

go?對象池化組件?bytebufferpool使用詳解_Golang

作者:FfFJ ? 更新時間: 2022-11-20 編程語言

1. 針對問題

在編程開發的過程中,我們經常會有創建同類對象的場景,這樣的操作可能會對性能產生影響,一個比較常見的做法是使用對象池,需要創建對象的時候,我們先從對象池中查找,如果有空閑對象,則從對象池中移除這個對象并將其返回給調用者使用,只有在池中無空閑對象的時候,才會真正創建一個新對象

另一方面,對于使用完的對象,我們并不會對它進行銷毀,而是將它放回到對象池以供后續使用,使用對象池在頻繁創建和銷毀對象的情況下,能大幅的提升性能,同時為了避免對象池中的對象占用過多的內存,對象池一般還配有特定的清理策略,Go的標準庫sync.Pool就是這樣一個例子,sync.Pool 中的對象會被垃圾回收清理掉

這類對象中,有一種比較特殊的是字節切片,在做字符串拼接的時候,為了拼接高效,我們通常將中間結果存放在一個字節緩沖中,拼接完之后,再從字節緩沖區生成字符串

Go標準庫bytes.Buffer封裝字節切片,提供一些使用接口,我們知道切片的容量是有限的,容量不足時需要進行擴容,而頻繁的擴容容易造成性能抖動

bytebufferpool實現了自己的Buffer類型,并引入一個簡單的算法降低擴容帶來的性能損失

2. 使用方法

bytebufferpool的接入很輕量

func main() {
   bf := bytebufferpool.Get()
   bf.WriteString("Hello")
   bf.WriteString(" World!!")
   fmt.Println(bf.String())
}

上面的這種用法使用的是defaultPoolbytebufferpoolPool對象是公開的,也可以自行新建

3. 源碼剖析

bytebufferpool是如何做到最大程度減小內存分配和浪費的呢,先宏觀的看整個Pool的定義,然后細化到相關的方法,就可以找到答案

bytebufferpoolPool結構體的定義為

type Pool struct {
   calls       [steps]uint64
   calibrating uint64
   defaultSize uint64
   maxSize     uint64
   pool sync.Pool
}

其中calls存儲了某一個區間內不同大小對象的個數,calibrating是一個標志位,標志當前Pool是否在重新規劃中,defaultSize是元素新建時的默認大小,它的選取邏輯是當前calls中出現次數最多的對象對應的區間最大值,這樣可以防止從對象池中撈取之后的頻繁擴容,maxSize限制了放入Pool中的最大元素的大小,防止因為一些很大的對象占用過多的內存

bytebufferpool中定義了一些和defaultSizemaxSize計算相關的常量

const (
   minBitSize = 6 // 2**6=64 is a CPU cache line size
   steps      = 20
   minSize = 1 << minBitSize
   maxSize = 1 << (minBitSize + steps - 1)
   calibrateCallsThreshold = 42000
   maxPercentile           = 0.95
)

其中minBitSize表示的是第一個區間對象大小的最大值(2的xx次方-1),在bytebufferpool中,將對象大小分為20個區間,也就是steps,第一個區間為[0, 2^6-1],第二個為[2^6, 2^7-1]...,依此類推

calibrateCallsThreshold表示如果某個區間內對象的數量超過這個閾值,則對Pool中的變量進行重新的計算,maxPercentile用于計算Pool中的maxSize,表示前95%的元素大小

bytebufferpool中的方法也比較少,核心的是GetPut方法

  • Get
func (p *Pool) Get() *ByteBuffer {
   v := p.pool.Get()
   if v != nil {
      return v.(*ByteBuffer)
   }
   return &ByteBuffer{
      B: make([]byte, 0, atomic.LoadUint64(&p.defaultSize)),
   }
}

可以看到,如果對象池中沒有對象的話,會申請defaultSize大小的切片返回

  • Put
func (p *Pool) Put(b *ByteBuffer) {
   idx := index(len(b.B))
   if atomic.AddUint64(&p.calls[idx], 1) > calibrateCallsThreshold {
      p.calibrate()
   }
   maxSize := int(atomic.LoadUint64(&p.maxSize))
   if maxSize == 0 || cap(b.B) <= maxSize {
      b.Reset()
      p.pool.Put(b)
   }
}

Put方法會比較麻煩,我們分步來看

  • 計算放入元素在calls數組中的位置
func index(n int) int {
   n--
   n >>= minBitSize
   idx := 0
   for n > 0 {
      n >>= 1
      idx++
   }
   if idx >= steps {
      idx = steps - 1
   }
   return idx
}

這里的邏輯就是先將長度右移minBitSize,如果依然大于0,則每次右移一位,idx加1,最后如果idx超出了總的steps(20),則位置就在最后一個區間

  • 判斷當前區間放入元素的個數是否超過了calibrateCallsThreshold指定的閾值,超過則重新計算Pool中元素的值
func (p *Pool) calibrate() {
   // 如果正在重新計算,則返回,控制多并發
   if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) {
      return
   }
   // 計算每一段區間中的元素個數 & 元素總個數
   a := make(callSizes, 0, steps)
   var callsSum uint64
   for i := uint64(0); i < steps; i++ {
      calls := atomic.SwapUint64(&p.calls[i], 0)
      callsSum += calls
      a = append(a, callSize{
         calls: calls,
         size:  minSize << i,
      })
   }
   // 按照對象元素的個數從大到小排序
   sort.Sort(a)
   // defaultSize 為內部切片的默認大小,減少擴容次數
   // maxSize 限制放入pool中的最大元素大小
   defaultSize := a[0].size
   maxSize := defaultSize
   // 將前95%元素中的最大size給maxSize
   maxSum := uint64(float64(callsSum) * maxPercentile)
   callsSum = 0
   for i := 0; i < steps; i++ {
      if callsSum > maxSum {
         break
      }
      callsSum += a[i].calls
      size := a[i].size
      if size > maxSize {
         maxSize = size
      }
   }
   // 對defaultSize和maxSize進行賦值
   atomic.StoreUint64(&p.defaultSize, defaultSize)
   atomic.StoreUint64(&p.maxSize, maxSize)
   atomic.StoreUint64(&p.calibrating, 0)
}
  • 判斷當前放入元素的大小是否超過了maxSize,超過則不放入對象池中

原文鏈接:https://juejin.cn/post/7150528125841965093

欄目分類
最近更新