go 對象池化組件 bytebufferpool使用詳解
1. 針對問題
在編程開發(fā)的過程中,我們經常會有創(chuàng)建同類對象的場景,這樣的操作可能會對性能產生影響,一個比較常見的做法是使用對象池,需要創(chuàng)建對象的時候,我們先從對象池中查找,如果有空閑對象,則從對象池中移除這個對象并將其返回給調用者使用,只有在池中無空閑對象的時候,才會真正創(chuàng)建一個新對象
另一方面,對于使用完的對象,我們并不會對它進行銷毀,而是將它放回到對象池以供后續(xù)使用,使用對象池在頻繁創(chuàng)建和銷毀對象的情況下,能大幅的提升性能,同時為了避免對象池中的對象占用過多的內存,對象池一般還配有特定的清理策略,Go的標準庫sync.Pool
就是這樣一個例子,sync.Pool
中的對象會被垃圾回收清理掉
這類對象中,有一種比較特殊的是字節(jié)切片,在做字符串拼接的時候,為了拼接高效,我們通常將中間結果存放在一個字節(jié)緩沖中,拼接完之后,再從字節(jié)緩沖區(qū)生成字符串
Go標準庫bytes.Buffer
封裝字節(jié)切片,提供一些使用接口,我們知道切片的容量是有限的,容量不足時需要進行擴容,而頻繁的擴容容易造成性能抖動
bytebufferpool
實現(xiàn)了自己的Buffer
類型,并引入一個簡單的算法降低擴容帶來的性能損失
2. 使用方法
bytebufferpool
的接入很輕量
func main() { bf := bytebufferpool.Get() bf.WriteString("Hello") bf.WriteString(" World!!") fmt.Println(bf.String()) }
上面的這種用法使用的是defaultPool
,bytebufferpool
的Pool
對象是公開的,也可以自行新建
3. 源碼剖析
bytebufferpool
是如何做到最大程度減小內存分配和浪費的呢,先宏觀的看整個Pool
的定義,然后細化到相關的方法,就可以找到答案
bytebufferpool
中Pool
結構體的定義為
type Pool struct { calls [steps]uint64 calibrating uint64 defaultSize uint64 maxSize uint64 pool sync.Pool }
其中calls
存儲了某一個區(qū)間內不同大小對象的個數(shù),calibrating
是一個標志位,標志當前Pool
是否在重新規(guī)劃中,defaultSize
是元素新建時的默認大小,它的選取邏輯是當前calls
中出現(xiàn)次數(shù)最多的對象對應的區(qū)間最大值,這樣可以防止從對象池中撈取之后的頻繁擴容,maxSize
限制了放入Pool
中的最大元素的大小,防止因為一些很大的對象占用過多的內存
bytebufferpool
中定義了一些和defaultSize
及maxSize
計算相關的常量
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
表示的是第一個區(qū)間對象大小的最大值(2的xx次方-1),在bytebufferpool
中,將對象大小分為20個區(qū)間,也就是steps
,第一個區(qū)間為[0, 2^6-1]
,第二個為[2^6, 2^7-1]
...,依此類推
calibrateCallsThreshold
表示如果某個區(qū)間內對象的數(shù)量超過這個閾值,則對Pool
中的變量進行重新的計算,maxPercentile
用于計算Pool
中的maxSize
,表示前95%
的元素大小
bytebufferpool
中的方法也比較少,核心的是Get
和Put
方法
- 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
數(shù)組中的位置
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),則位置就在最后一個區(qū)間
- 判斷當前區(qū)間放入元素的個數(shù)是否超過了
calibrateCallsThreshold
指定的閾值,超過則重新計算Pool
中元素的值
func (p *Pool) calibrate() { // 如果正在重新計算,則返回,控制多并發(fā) if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) { return } // 計算每一段區(qū)間中的元素個數(shù) & 元素總個數(shù) 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, }) } // 按照對象元素的個數(shù)從大到小排序 sort.Sort(a) // defaultSize 為內部切片的默認大小,減少擴容次數(shù) // 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
,超過則不放入對象池中
以上就是go 對象池化組件 bytebufferpool使用詳解的詳細內容,更多關于go bytebufferpool的資料請關注腳本之家其它相關文章!
相關文章
go語法入門泛型type?parameters簡稱T(類型形參)兩種場景使用
這篇文章主要為大家介紹了go語法入門泛型type?parameters簡稱T(類型形參)兩種場景使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09go json數(shù)據轉發(fā)的實現(xiàn)代碼
這篇文章主要介紹了go json數(shù)據轉發(fā)的實現(xiàn)代碼,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09sublime text3解決Gosublime無法自動補全代碼的問題
本文主要介紹了sublime text3解決Gosublime無法自動補全代碼的問題,文中通過示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01