Created
November 28, 2025 05:06
-
-
Save lbe/583661f8f276734b221188e199d0c930 to your computer and use it in GitHub Desktop.
Go benchmark that compares several pooling strategies
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Pool Benchmarks (Standalone Gist) | |
| // | |
| // This single-file Go benchmark compares several pooling strategies: | |
| // - Interface sync.Pool with pointer indirection (e.g., *[]byte, *bytes.Buffer) | |
| // - Generic value slice pool (SlicePool[T]) | |
| // - Generic pointer pool (PtrPool[T]) | |
| // - Minimal ResettablePool (calls Reset() on Put) | |
| // | |
| // Usage: | |
| // | |
| // go test -bench . -benchmem -run '^$' | |
| // | |
| // Benchmark groups: | |
| // | |
| // go test -bench 'BufPool' -benchmem -run '^$' | |
| // go test -bench 'BufferPool' -benchmem -run '^$' | |
| // go test -bench 'Null(String|Int64)Pool_(GetPut|Parallel)$' -benchmem -run '^$' | |
| // go test -bench 'MD5_(GetPut|Parallel)$' -benchmem -run '^$' | |
| // go test -bench 'GalleryImage_(GetPut|Parallel)$' -benchmem -run '^$' | |
| // | |
| // Notes: | |
| // - The file is standalone (standard library only). | |
| // - Results vary by CPU/Go version; focus on relative comparisons. | |
| // - Generic value slice pools allocate; pointer pools avoid allocations. | |
| package poolbench | |
| import ( | |
| "bytes" | |
| "crypto/md5" | |
| "database/sql" | |
| "image" | |
| "image/color" | |
| "sync" | |
| "testing" | |
| ) | |
| // Dummy references to satisfy standalone linters scanning file in isolation. | |
| var ( | |
| _ = bytes.MinRead | |
| _ = md5.BlockSize | |
| _ = sql.ErrNoRows | |
| _ = image.ZP | |
| _ = color.RGBAModel | |
| _ = testing.AllocsPerRun | |
| ) | |
| // --- Minimal resettable pool (standalone) --- | |
| type Resettable interface{ Reset() } | |
| type ResettablePool struct{ p sync.Pool } | |
| func NewResettablePool(newFn func() interface{}) *ResettablePool { | |
| return &ResettablePool{p: sync.Pool{New: func() any { return newFn() }}} | |
| } | |
| func (rp *ResettablePool) Get() interface{} { return rp.p.Get() } | |
| func (rp *ResettablePool) Put(v interface{}) { | |
| if r, ok := v.(Resettable); ok { | |
| r.Reset() | |
| } | |
| rp.p.Put(v) | |
| } | |
| // --- Generic slice and pointer pools --- | |
| type SlicePool[T any] struct{ pool sync.Pool } | |
| func NewSlicePool[T any](newFn func() T) *SlicePool[T] { | |
| return &SlicePool[T]{pool: sync.Pool{New: func() any { return newFn() }}} | |
| } | |
| func (p *SlicePool[T]) Get() T { return p.pool.Get().(T) } | |
| func (p *SlicePool[T]) Put(v T) { p.pool.Put(v) } | |
| type PtrPool[T any] struct{ pool sync.Pool } | |
| func NewPtrPool[T any](newFn func() T) *PtrPool[T] { | |
| return &PtrPool[T]{pool: sync.Pool{New: func() any { return newFn() }}} | |
| } | |
| func (p *PtrPool[T]) Get() T { return p.pool.Get().(T) } | |
| func (p *PtrPool[T]) Put(v T) { p.pool.Put(v) } | |
| // --- Buffer and byte slice pools --- | |
| var ( | |
| interfaceBufPool = sync.Pool{New: func() any { b := make([]byte, 32*1024); return &b }} | |
| genericBufPool = NewSlicePool(func() []byte { return make([]byte, 32*1024) }) | |
| ) | |
| func touchBuffer(b []byte) { | |
| for i := 0; i < 16; i++ { | |
| b[i] = byte(i) | |
| } | |
| _ = b[0] | |
| } | |
| func BenchmarkInterfaceBufPool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| bufPtr := interfaceBufPool.Get().(*[]byte) | |
| buf := *bufPtr | |
| touchBuffer(buf) | |
| interfaceBufPool.Put(bufPtr) | |
| } | |
| } | |
| func BenchmarkGenericBufPool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| buf := genericBufPool.Get() | |
| touchBuffer(buf) | |
| genericBufPool.Put(buf) | |
| } | |
| } | |
| func BenchmarkInterfaceBufPool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| bufPtr := interfaceBufPool.Get().(*[]byte) | |
| buf := *bufPtr | |
| touchBuffer(buf) | |
| interfaceBufPool.Put(bufPtr) | |
| } | |
| }) | |
| } | |
| func BenchmarkGenericBufPool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| buf := genericBufPool.Get() | |
| touchBuffer(buf) | |
| genericBufPool.Put(buf) | |
| } | |
| }) | |
| } | |
| func BenchmarkInterfaceBufPool_Zeroing(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| bufPtr := interfaceBufPool.Get().(*[]byte) | |
| buf := *bufPtr | |
| for j := range buf { | |
| buf[j] = 0 | |
| } | |
| interfaceBufPool.Put(bufPtr) | |
| } | |
| } | |
| func BenchmarkGenericBufPool_Zeroing(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| buf := genericBufPool.Get() | |
| for j := range buf { | |
| buf[j] = 0 | |
| } | |
| genericBufPool.Put(buf) | |
| } | |
| } | |
| // --- *bytes.Buffer pointer pools --- | |
| var ( | |
| interfaceBufferPool = sync.Pool{New: func() any { return &bytes.Buffer{} }} | |
| genericPtrBufferPool = NewPtrPool(func() *bytes.Buffer { return &bytes.Buffer{} }) | |
| ) | |
| func touchBytesBuffer(buf *bytes.Buffer) { | |
| buf.Reset() | |
| buf.Write([]byte("abcdefg")) | |
| _ = buf.Len() | |
| } | |
| func BenchmarkInterfaceBufferPool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := interfaceBufferPool.Get().(*bytes.Buffer) | |
| touchBytesBuffer(v) | |
| interfaceBufferPool.Put(v) | |
| } | |
| } | |
| func BenchmarkGenericPtrBufferPool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := genericPtrBufferPool.Get() | |
| touchBytesBuffer(v) | |
| genericPtrBufferPool.Put(v) | |
| } | |
| } | |
| func BenchmarkInterfaceBufferPool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := interfaceBufferPool.Get().(*bytes.Buffer) | |
| touchBytesBuffer(v) | |
| interfaceBufferPool.Put(v) | |
| } | |
| }) | |
| } | |
| func BenchmarkGenericPtrBufferPool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := genericPtrBufferPool.Get() | |
| touchBytesBuffer(v) | |
| genericPtrBufferPool.Put(v) | |
| } | |
| }) | |
| } | |
| // --- sql.NullString / sql.NullInt64 pointer pools --- | |
| var ( | |
| interfaceNullStringPool = sync.Pool{New: func() any { return &sql.NullString{} }} | |
| genericPtrNullStringPool = NewPtrPool(func() *sql.NullString { return &sql.NullString{} }) | |
| ) | |
| func touchNullString(ns *sql.NullString) { ns.String = "x"; ns.Valid = true } | |
| func BenchmarkInterfaceNullStringPool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := interfaceNullStringPool.Get().(*sql.NullString) | |
| touchNullString(v) | |
| interfaceNullStringPool.Put(v) | |
| } | |
| } | |
| func BenchmarkGenericPtrNullStringPool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := genericPtrNullStringPool.Get() | |
| touchNullString(v) | |
| genericPtrNullStringPool.Put(v) | |
| } | |
| } | |
| func BenchmarkInterfaceNullStringPool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := interfaceNullStringPool.Get().(*sql.NullString) | |
| touchNullString(v) | |
| interfaceNullStringPool.Put(v) | |
| } | |
| }) | |
| } | |
| func BenchmarkGenericPtrNullStringPool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := genericPtrNullStringPool.Get() | |
| touchNullString(v) | |
| genericPtrNullStringPool.Put(v) | |
| } | |
| }) | |
| } | |
| var ( | |
| interfaceNullInt64Pool = sync.Pool{New: func() any { return &sql.NullInt64{} }} | |
| genericPtrNullInt64Pool = NewPtrPool(func() *sql.NullInt64 { return &sql.NullInt64{} }) | |
| ) | |
| func touchNullInt64(ni *sql.NullInt64) { ni.Int64 = 1; ni.Valid = true } | |
| func BenchmarkInterfaceNullInt64Pool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := interfaceNullInt64Pool.Get().(*sql.NullInt64) | |
| touchNullInt64(v) | |
| interfaceNullInt64Pool.Put(v) | |
| } | |
| } | |
| func BenchmarkGenericPtrNullInt64Pool_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := genericPtrNullInt64Pool.Get() | |
| touchNullInt64(v) | |
| genericPtrNullInt64Pool.Put(v) | |
| } | |
| } | |
| func BenchmarkInterfaceNullInt64Pool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := interfaceNullInt64Pool.Get().(*sql.NullInt64) | |
| touchNullInt64(v) | |
| interfaceNullInt64Pool.Put(v) | |
| } | |
| }) | |
| } | |
| func BenchmarkGenericPtrNullInt64Pool_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := genericPtrNullInt64Pool.Get() | |
| touchNullInt64(v) | |
| genericPtrNullInt64Pool.Put(v) | |
| } | |
| }) | |
| } | |
| // --- MD5 pools: resettable vs generic pointer of Resettable iface --- | |
| var md5ResettablePool = NewResettablePool(func() interface{} { return md5.New() }) | |
| type hashIface = interface{ Reset() } | |
| var md5GenericIfacePool = NewPtrPool(func() hashIface { return md5.New() }) | |
| func touchMD5(h hashIface) { h.Reset() } | |
| func BenchmarkResettablePool_MD5_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := md5ResettablePool.Get().(hashIface) | |
| touchMD5(v) | |
| md5ResettablePool.Put(v) | |
| } | |
| } | |
| func BenchmarkGenericPtrPool_MD5_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := md5GenericIfacePool.Get() | |
| touchMD5(v) | |
| md5GenericIfacePool.Put(v) | |
| } | |
| } | |
| func BenchmarkResettablePool_MD5_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := md5ResettablePool.Get().(hashIface) | |
| touchMD5(v) | |
| md5ResettablePool.Put(v) | |
| } | |
| }) | |
| } | |
| func BenchmarkGenericPtrPool_MD5_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := md5GenericIfacePool.Get() | |
| touchMD5(v) | |
| md5GenericIfacePool.Put(v) | |
| } | |
| }) | |
| } | |
| // --- galleryImage type + pools --- | |
| type galleryImage struct { | |
| img *image.RGBA | |
| } | |
| func (g *galleryImage) Reset() { | |
| // Cheap reset: clear pixel buffer | |
| if g.img != nil && g.img.Pix != nil { | |
| for i := range g.img.Pix { | |
| g.img.Pix[i] = 0 | |
| } | |
| } | |
| // Also touch bounds to simulate light usage | |
| _ = g.img.Bounds() | |
| if g.img != nil { | |
| g.img.Set(0, 0, color.RGBA{0, 0, 0, 0}) | |
| } | |
| } | |
| var galleryImagePool = NewResettablePool(func() interface{} { | |
| return &galleryImage{img: image.NewRGBA(image.Rect(0, 0, 1920, 1080))} | |
| }) | |
| var genericPtrGalleryImagePool = NewPtrPool(func() *galleryImage { | |
| return &galleryImage{img: image.NewRGBA(image.Rect(0, 0, 1920, 1080))} | |
| }) | |
| func touchGalleryImage(gi *galleryImage) { gi.Reset() } | |
| func BenchmarkResettablePool_GalleryImage_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := galleryImagePool.Get().(*galleryImage) | |
| touchGalleryImage(v) | |
| galleryImagePool.Put(v) | |
| } | |
| } | |
| func BenchmarkGenericPtrPool_GalleryImage_GetPut(b *testing.B) { | |
| for i := 0; i < b.N; i++ { | |
| v := genericPtrGalleryImagePool.Get() | |
| touchGalleryImage(v) | |
| genericPtrGalleryImagePool.Put(v) | |
| } | |
| } | |
| func BenchmarkResettablePool_GalleryImage_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := galleryImagePool.Get().(*galleryImage) | |
| touchGalleryImage(v) | |
| galleryImagePool.Put(v) | |
| } | |
| }) | |
| } | |
| func BenchmarkGenericPtrPool_GalleryImage_Parallel(b *testing.B) { | |
| b.RunParallel(func(pb *testing.PB) { | |
| for pb.Next() { | |
| v := genericPtrGalleryImagePool.Get() | |
| touchGalleryImage(v) | |
| genericPtrGalleryImagePool.Put(v) | |
| } | |
| }) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment