SoFunction
Updated on 2025-03-03

go object pooling component bytebufferpool usage detailed explanation

1. Targeting the problem

During the programming and development process, we often have scenarios of creating similar objects. Such operations may have an impact on performance. A more common practice is to use an object pool. When we need to create an object, we first look up from the object pool. If there are free objects, remove the object from the object pool and return it to the caller for use. Only when there are no free objects in the pool will a new object be created.

On the other hand, for used objects, we will not destroy them, but put them back to the object pool for subsequent use. Using the object pool can greatly improve performance when frequently creating and destroying objects. At the same time, in order to avoid the objects in the object pool occupying too much memory, the object pool is generally equipped with a specific cleaning strategy, Go's standard libraryThis is an example of this.The objects in it will be cleaned up by garbage collection

Among these objects, there is a special one that is byte slice. When doing string splicing, in order to efficient splicing, we usually store the intermediate result in a byte buffer. After splicing, we will generate a string from the byte buffer.

Go standard libraryEncapsulated byte slices and provide some usage interfaces. We know that the capacity of slices is limited. When the capacity is insufficient, capacity expansion is required. Frequent capacity expansion can easily cause performance jitter.

bytebufferpoolRealize your ownBufferType and introduce a simple algorithm to reduce the performance losses caused by expansion

2. How to use

bytebufferpoolVery light access

func main() {
   bf := ()
   ("Hello")
   (" World!!")
   (())
}

The above usage usesdefaultPoolbytebufferpoolofPoolThe object is public or can be created by yourself

3. Source code analysis

bytebufferpoolHow to minimize memory allocation and waste? Let’s look at the entire macro first.PoolThen refine the definition of the relevant methods to find the answer

bytebufferpoolmiddlePoolThe definition of a structure is

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

incallsStores the number of objects of different sizes in a certain interval.calibratingis a flag bit that marks the current onePoolIs it being re-planned?defaultSizeIt is the default size when the element is newly created, and its selection logic is the current one.callsThe maximum interval value corresponding to the object with the most occurrences in the object, which can prevent frequent expansion after being fetched from the object pool.maxSizeRestricted to placePoolThe size of the largest element in prevents excessive memory from taking up some large objects

bytebufferpoolSome anddefaultSizeandmaxSizeCalculate related constants

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
)

inminBitSizeIt represents the maximum value of the first interval object size (2 to the power of xx-1), inbytebufferpoolIn the object size is divided into 20 intervals, that issteps, the first interval is[0, 2^6-1], the second one is[2^6, 2^7-1]..., and so on

calibrateCallsThresholdIndicates that if the number of objects in a certain interval exceeds this threshold,PoolThe variables inmaxPercentileUsed for calculationPoolIn-housemaxSize, indicating the previous95%Element size

bytebufferpoolThere are fewer methods in it, the core isGetandPutmethod

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

You can see that if there are no objects in the object pool, you will apply.defaultSizeSlices of size return

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

The Put method will be more troublesome, let's take a look at it in steps

  • Calculate the element incallsPosition in the array
func index(n int) int {
   n--
   n >>= minBitSize
   idx := 0
   for n > 0 {
      n >>= 1
      idx++
   }
   if idx >= steps {
      idx = steps - 1
   }
   return idx
}

The logic here is to move the length right firstminBitSize, if it is still greater than 0, then move one right one each time, add idx to 1, and finally if idx exceeds the totalsteps(20), then the position is in the last interval

  • Determine whether the number of elements placed in the current interval exceeds thecalibrateCallsThresholdThe specified threshold value is exceeded and recalculate itPoolThe value of the element in
func (p *Pool) calibrate() {
   // If recalculation is being performed, return to control multiple concurrency   if !atomic.CompareAndSwapUint64(&amp;, 0, 1) {
      return
   }
   // Calculate the number of elements & total number of elements in each section   a := make(callSizes, 0, steps)
   var callsSum uint64
   for i := uint64(0); i &lt; steps; i++ {
      calls := atomic.SwapUint64(&amp;[i], 0)
      callsSum += calls
      a = append(a, callSize{
         calls: calls,
         size:  minSize &lt;&lt; i,
      })
   }
   // Sort from large to small by the number of object elements   (a)
   // defaultSize is the default size of the internal slice, reducing the number of expansions   // maxSize limits the maximum element size placed in the pool   defaultSize := a[0].size
   maxSize := defaultSize
   // Give the maximum size in the first 95% element to maxSize   maxSum := uint64(float64(callsSum) * maxPercentile)
   callsSum = 0
   for i := 0; i &lt; steps; i++ {
      if callsSum &gt; maxSum {
         break
      }
      callsSum += a[i].calls
      size := a[i].size
      if size &gt; maxSize {
         maxSize = size
      }
   }
   // Assign defaultSize and maxSize   atomic.StoreUint64(&amp;, defaultSize)
   atomic.StoreUint64(&amp;, maxSize)
   atomic.StoreUint64(&amp;, 0)
}
  • Determine whether the size of the currently placed element exceeds themaxSize, if it exceeds the value, it will not be placed in the object pool

The above is the detailed explanation of the use of go object pooling component bytebufferpool. For more information about go bytebufferpool, please follow my other related articles!