NOTE

2.5 sync.pool

What sync.Pool is, why temporary object caching can reduce allocation and GC pressure, usage, fmt integration, source structures, Get/Put, GC cleanup, and summary.

GoCreated Updated 1 min readhistorical

This is a historical learning note and may contain outdated or incomplete understanding.

1. What It Is

A pool that stores temporary objects. temp means that an object may be reclaimed, so this pool is actually a cache.

2. Why It Is Needed

Cache temporarily unused objects and use them directly the next time they are needed. This avoids memory allocation and reduces GC pressure.

3. How to Use It

package main

import (
	"fmt"
	"sync"
)

var pool *sync.Pool

type Person struct {
	Name string
}

func (p *Person)Reset() {
    p.Name = ""
}

// Initialize the pool. The New function needs to be configured
func initPool() {
	pool = &sync.Pool{
		New: func() interface{} {
			fmt.Println("Creating a new Person")
			return new(Person)
		},
	}
}

func main() {
	initPool()
	// First Get call: there is no cached object, so call New to create one
	p := pool.Get().(*Person)
	fmt.Println("First retrieval from pool:", p)
    // Initialize
    p.Reset()
	p.Name = "first"
	fmt.Printf("Set p.Name = %s\n", p.Name)
	// Put it back into the pool after use
	pool.Put(p)
	// Second Get call: there is a cached object, so return it directly
	fmt.Println("Pool already has one object: &{first}, call Get: ", pool.Get().(*Person))
	// Third Get call: there is no cached object, so call New to create one
	fmt.Println("Pool has no objects left, call Get: ", pool.Get().(*Person))
}

3.1. fmt

  • Printf
func Printf(format string, a ...interface{}) (n int, err error) {
    // Output to os.Stdout -- standard output
	return Fprintf(os.Stdout, format, a...)
}
  • Fprintf
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()// create pp
	p.doPrintf(format, a)
	n, err = w.Write(p.buf)
	p.free()// return the pp pointer to the Pool
	return
}

func newPrinter() *pp {
    // Actually obtains pp from the Pool
	p := ppFree.Get().(*pp)
	p.panicking = false
	p.erroring = false
	p.wrapErrs = false
	p.fmt.init(&p.buf)
	return p
}

var ppFree = sync.Pool{
	New: func() interface{} { return new(pp) },
}


func (p *pp) free() {
	if cap(p.buf) > 64<<10 {
		return
	}

    // Clear some object fields before returning it to the Pool
	p.buf = p.buf[:0]
	p.arg = nil
	p.value = reflect.Value{}
	p.wrappedErr = nil
	ppFree.Put(p)
}

4. Source Code Analysis

4.1. Data Structures

  • Pool
type Pool struct {
	noCopy noCopy

    // Local queue for each P; actual type is [P]poolLocal
	local     unsafe.Pointer // points to the [P]poolLocal array
	// Size of [P]poolLocal
	localSize uintptr        // size of the local array

    // When a GC cycle arrives, victim and victimSize respectively "take over" local and localSize. The victim mechanism reduces performance jitter caused by cold start after GC and makes object allocation smoother
	victim     unsafe.Pointer // local from previous cycle
	victimSize uintptr        // size of victims array

	// Custom object-creation callback. Called when there are no available objects in the pool
	New func() interface{}
}
  • poolLocal
// Local per-P Pool appendix.
type poolLocalInternal struct {
	private interface{} // P's private cache; no lock is needed when using it
	shared  poolChain   // Shared cache. The local P can pushHead/popHead; other Ps can only popTail
}

type poolLocal struct {
	poolLocalInternal

	// Pad poolLocal to a multiple of two cache lines to prevent false sharing.
	// Each cache line has 64 bytes. Our processors generally have 32 KB cache, so there are 32 * 1024 / 64 = 512 cache lines
	// False-sharing padding only; prevents multiple poolLocalInternal values from being allocated on one cache line
	pad [128 - unsafe.Sizeof(poolLocalInternal{})%128]byte
}
  • poolChain
type poolChain struct {
	// Only the producer pushes here, so no lock is needed
	head *poolChainElt

	// Reads and writes require atomic control. Pop from here
	tail *poolChainElt
}

type poolChainElt struct {
	poolDequeue

	// next is written by the producer and read by consumers, so it only changes from nil to non-nil
	// prev is written by consumers and read by the producer, so it only changes from non-nil to nil
	next, prev *poolChainElt
}


// poolDequeue is implemented as a fixed-size lock-free (atomic implementation) ring queue with a single producer and multiple consumers. The underlying storage is an array, with two pointers marking head and tail. The producer can insert at head and delete from head, while consumers can only delete from tail.
type poolDequeue struct {

    // headTail points to the head and tail of the queue. Bit operations store head and tail in the headTail variable.
	// headTail contains a 32-bit head and a 32-bit tail pointer. Both values have been reduced modulo len(vals)-1.
	// tail is the oldest data in the queue, and head points to the next slot to be filled
    // The valid range of slots is [tail, head), owned by consumers.
	headTail uint64

	// vals is a ring queue storing interface{} values; its size must be a power of 2
	// If a slot is empty, vals[i].typ is empty; otherwise it is non-empty.
	// A slot is declared invalid when tail no longer points to it and vals[i].typ is nil
	// Set to nil by the consumer and read by the producer
	vals []eface
}

4.2. Methods

4.2.1. Get

func (p *Pool) Get() interface{} {
    // ......
    // Call p.pin() to bind the current goroutine to P, disable preemption, and return the poolLocal corresponding to the current P together with pid
	l, pid := p.pin()
	// Then directly take l.private, assign it to x, and set l.private to nil
	x := l.private
	l.private = nil
	if x == nil {
	    // If x is empty, try to pop an object from the head of l.shared and assign it to x
		x, _ = l.shared.popHead()
		if x == nil {
		    // If x is still empty, call getSlow to try to "steal" an object from the tail of another P's shared deque
			x = p.getSlow(pid)
		}
	}
	// Pool-related operations are complete; call runtime_procUnpin() to re-enable preemption
	runtime_procUnpin()
    // ......
    // If no cached object was obtained, directly call the preconfigured New function to create one
	if x == nil && p.New != nil {
		x = p.New()
	}
	return x
}

4.2.2. Put

// src/sync/pool.go

// Put adds an object to the Pool
func (p *Pool) Put(x interface{}) {
	if x == nil {
		return
	}
	// ……
	// First bind g and P, then try to assign x to the private field.
	l, _ := p.pin()
	if l.private == nil {
		l.private = x
		x = nil
	}
	// If that fails, call pushHead to try to put it into the deque maintained by the shared field.
	if x != nil {
		l.shared.pushHead(x)
	}
	runtime_procUnpin()
    //…… 
}

4.3. GC

In all pooling techniques, cached objects are cleared at some point.

The init function in pool.go:

func init() {
    // Register the function that clears Pool objects when GC occurs
	runtime_registerPoolCleanup(poolCleanup)
}
  • poolCleanup
func poolCleanup() {
    // Clear old pools
	for _, p := range oldPools {
		p.victim = nil
		p.victimSize = 0
	}

	// Move primary cache to victim cache.
	// Move all pools into victim
	for _, p := range allPools {
		p.victim = p.local
		p.victimSize = p.localSize
		p.local = nil
		p.localSize = 0
	}

	oldPools, allPools = allPools, nil
}

5. Summary

  • sync.Pool is goroutine-safe and very convenient to use. After configuring the New function, call Get to obtain an object and Put to return it
  • Do not make any assumptions about an object returned by Get; a better practice is to “clear” the object when returning it
  • The lifetime of objects in a Pool is affected by GC, so it is not suitable for connection pools, because connection pools need to manage object lifetimes themselves

6. References

Discussion

Sign in with GitHub to comment. Discussions are stored as GitHub Issues.View on GitHub