English | 中文
BytePool is a high-performance Go memory pool library designed to solve the "don't know when to release memory" scenario. It automatically manages memory lifecycle through reference counting mechanism, preventing memory leaks.
- Automatic Reference Counting: No need to manually track memory release timing, automatically reclaimed when reference count reaches zero
- Tiered Memory Pool: Supports multi-tier memory allocation, reducing memory fragmentation and improving allocation efficiency
- Zero-Copy Design: Optimized based on
sync.Pool, avoiding unnecessary memory copying - Thread-Safe: Fully concurrent-safe, suitable for high-concurrency scenarios
- Built-in Statistics: Real-time monitoring of memory usage with expvar integration support
- High Performance: Memory allocation performance superior to direct
make([]byte, size), statistics add only 11ns overhead
Common memory management challenges in Go development:
When to use sync.Pool:
If your scenario clearly knows when to release memory, we recommend using the standard library's sync.Pool directly. This library also provides a generic version of sync.Pool (Pool[T]) that offers better type safety, which you can try.
Limitations of Traditional Solutions:
- Solution 1: Independent memory pools per package → Memory waste, multiple copies
- Solution 2: Shared memory pool but unknown release timing → Memory leak risks
BytePool's Solution: When you encounter "don't know when to release memory" scenarios, BytePool provides the perfect solution:
- Reference counting mechanism automatically manages memory lifecycle
- Tiered architecture optimizes allocation for different sizes
- Statistics help identify memory usage patterns and potential leaks
go get github.com/ixugo/bytepoolpackage main
import (
"fmt"
"github.com/ixugo/bytepool"
)
func main() {
// Create tiered memory pool (128B, 256B, 512B, 1KB, 2KB...)
pool := bytepool.NewPools(bytepool.SizePowerOfTwo())
// Method 1: Direct Get/Put usage
buf := pool.Get(100) // Get at least 100 bytes of memory
// Use buf...
pool.Put(buf) // Manual release
// Method 2: Use Buffer (Recommended)
buffer := pool.GetBuffer(200) // Automatic reference counting management
// Use buffer.Bytes()...
buffer.Release() // Reference count-1, auto-reclaimed when reaches 0
}There are two approaches for using reference counting:
Approach 1: Manual Reference Counting
// Create Buffer
buffer := pool.GetBuffer(1024)
// Increase reference
buffer.Retain() // Reference count = 2
// Use in different goroutines
go func() {
defer buffer.Release() // Reference count-1
// Use buffer...
}()
// Main goroutine release
buffer.Release() // Reference count-1, now 0, auto-reclaimed to poolApproach 2: Automatic Management with Bytes()
// Create Buffer
buffer := pool.GetBuffer(1024)
// Use Bytes() method, automatically increases reference count and returns release function
data, release := buffer.Bytes()
// Use in different goroutines
go func() {
defer release() // Automatically release reference
// Use data...
}()
// Main goroutine release
buffer.Release() // Release initial referenceBytePool includes powerful built-in statistics to help monitor and optimize memory usage:
// Enable expvar statistics
pool.Expvar("myapp_")
// When using Go's default http service, you can access http://localhost:6060/debug/vars to view statistics
// Or get in code
stats := pool.GetPoolStats()| Parameter | Type | Description |
|---|---|---|
pools |
map[int]map[string]int64 |
Detailed statistics for each tier, key is tier size |
pools[size].get |
int64 |
Number of get operations for this tier |
pools[size].put |
int64 |
Number of put operations for this tier |
discarded |
int64 |
Number of allocations discarded (exceeding max tier) |
total_get |
int64 |
Total valid get operations (pool allocations only) |
total_put |
int64 |
Total valid put operations (pool reclaims only) |
recent_lengths |
[]int |
Recent 256 get operation request lengths (ring buffer) |
Compare total_get and total_put to detect potential memory leaks:
total_get > total_put: Unreleased memory existstotal_get = total_put: Memory allocation balanced- Monitor
recent_lengthsto analyze memory allocation patterns
Main memory pool type that manages multi-tier memory allocation.
Memory buffer with reference counting, automatically manages lifecycle.
Generic version of sync.Pool that provides type-safe object pooling. If you clearly know when to release memory, you can use this type directly instead of BytePool.
Create a new tiered memory pool with optional configurations.
Parameters:
sizes: Memory tier array, must be positive and orderedopts: Optional configuration functions
Returns:
*BytePool: Memory pool instance
Example:
// Use predefined power-of-2 tiers with default lock-free ring queue
pool := bytepool.NewPools(bytepool.SizePowerOfTwo())
// Custom tiers with mutex-based ring queue for strong consistency
pool := bytepool.NewPools([]int{128, 512, 2048},
bytepool.WithRingQueueType(bytepool.MutexRingQueue))
// Use custom ring queue implementation
customQueue := bytepool.NewLockedRingQueue[int](512)
pool := bytepool.NewPools([]int{128, 512, 2048},
bytepool.WithRingQueue(customQueue))BytePool supports two types of ring queues for tracking recent allocation lengths:
Lock-Free Ring Queue (Default)
- Uses atomic operations for maximum performance
- ~2.3x faster than mutex-based version in single-threaded scenarios
- ~1.6x faster in concurrent scenarios
- May have slight data inconsistency during concurrent access (acceptable for statistics)
Mutex Ring Queue
- Uses mutex locks for strong consistency guarantees
- Provides additional operations like
Pop()andPeek() - Better for scenarios requiring strict data consistency
- Slightly higher memory overhead due to additional tracking fields
Performance Comparison:
| Operation | Lock-Free | Mutex-Based | Performance Ratio |
|---|---|---|---|
| Single Push | 8.2 ns/op | 19.1 ns/op | 2.3x faster |
| Concurrent Push | 63.9 ns/op | 102.0 ns/op | 1.6x faster |
| Bytes() | 1053 ns/op | 1666 ns/op | 1.6x faster |
| BytePool Integration | 186.1 ns/op | 291.8 ns/op | 1.6x faster |
When to use each:
- Lock-Free (Default): High-performance scenarios, statistical monitoring, write-heavy workloads
- Mutex-Based: Strong consistency requirements, need Pop/Peek operations, debugging scenarios
Returns power-of-2 tier sequence (128B to 2MB).
Returns tier sequence suitable for stream processing.
Create a new generic object pool.
Parameters:
fn: Function to create new objects
Returns:
*Pool[T]: Generic object pool instance
Example:
// Create string pool
stringPool := bytepool.NewPool(func() string { return "" })
// Create custom struct pool
type MyStruct struct { Data []byte }
structPool := bytepool.NewPool(func() *MyStruct {
return &MyStruct{Data: make([]byte, 1024)}
})Create a new lock-free ring queue.
Parameters:
size: Queue capacity, must be positive
Returns:
*RingQueue[T]: Lock-free ring queue instance
Create a new mutex-based ring queue.
Parameters:
size: Queue capacity, must be positive
Returns:
*LockedRingQueue[T]: Mutex-based ring queue instance
Example:
// Create lock-free ring queue for high performance
lockFreeQueue := bytepool.NewRingQueue[int](256)
// Create mutex-based ring queue for strong consistency
lockedQueue := bytepool.NewLockedRingQueue[string](128)Configure the type of ring queue to use.
Parameters:
queueType: EitherLockFreeRingQueue(default) orMutexRingQueue
Use a custom ring queue implementation.
Parameters:
ringQueuer: Custom ring queue that implements theRingQueuerinterface
Get byte slice of specified length from pool.
Parameters:
length: Required number of bytes
Returns:
[]byte: Byte slice with length, capacity may be larger
Return byte slice to pool.
Parameters:
buf: Byte slice to reclaim
Get Buffer with reference counting.
Parameters:
length: Required number of bytes
Returns:
*Buffer: Buffer instance with initial reference count of 1
Release Buffer (equivalent to buf.Release()).
Get byte slice of Buffer, automatically increases reference count.
Returns:
[]byte: Byte slicefunc(): Release function, automatically decreases reference count when called
Increase reference count.
Returns:
*Buffer: Returns self, supports method chaining
Decrease reference count, auto-reclaimed to pool when reaches 0.
Get current reference count.
Get detailed pool statistics.
Get number of discarded allocations.
Get all available tier sizes.
Enable expvar statistics with specified prefix.
Parameters:
prefix: Prefix for statistics variables
Returns:
*BytePool: Returns self, supports method chaining
Get object from pool.
Returns:
T: Object from pool or newly created object
Put object back to pool.
Parameters:
x: Object to put back to pool
Example:
pool := bytepool.NewPool(func() []byte { return make([]byte, 1024) })
// Get object
buf := pool.Get()
// Use buf...
// Put back to pool
pool.Put(buf)Both RingQueue[T] and LockedRingQueue[T] implement the RingQueuer interface and provide the following methods:
Add an element to the queue. If the queue is full, overwrites the oldest element.
Returns all current data in the queue in order (oldest to newest).
Returns the current number of elements in the queue.
Returns the queue capacity.
Checks if the queue is full.
Checks if the queue is empty.
Empties the queue.
Removes and returns the oldest element from the queue. Returns zero value and false if queue is empty.
Returns the oldest element without removing it. Returns zero value and false if queue is empty.
Example:
// Lock-free ring queue usage
queue := bytepool.NewRingQueue[int](5)
queue.Push(1)
queue.Push(2)
queue.Push(3)
data := queue.Bytes() // [1, 2, 3]
fmt.Printf("Length: %d, Capacity: %d\n", queue.Len(), queue.Cap())
// Mutex-based ring queue with additional operations
lockedQueue := bytepool.NewLockedRingQueue[string](3)
lockedQueue.Push("a")
lockedQueue.Push("b")
if item, ok := lockedQueue.Peek(); ok {
fmt.Printf("Oldest item: %s\n", item) // "a"
}
if item, ok := lockedQueue.Pop(); ok {
fmt.Printf("Popped: %s\n", item) // "a"
}Configure appropriate tiers based on application characteristics:
// Optimized for small objects
smallPool := bytepool.NewPools([]int{64, 128, 256, 512})
// Optimized for large file processing
largePool := bytepool.NewPools([]int{4096, 16384, 65536, 262144})// Periodically check memory usage
go func() {
ticker := time.NewTicker(time.Minute)
for range ticker.C {
stats := pool.GetPoolStats()
totalGet := stats["total_get"].(int64)
totalPut := stats["total_put"].(int64)
if totalGet != totalPut {
log.Printf("Memory leak warning: get=%d, put=%d", totalGet, totalPut)
}
}
}()When you clearly know when to release memory, you can use generic Pool[T]:
// Create buffer pool
bufferPool := bytepool.NewPool(func() *bytes.Buffer {
return &bytes.Buffer{}
})
// Usage
buf := bufferPool.Get()
buf.Reset() // Clear buffer
buf.WriteString("Hello, World!")
// Put back when done
bufferPool.Put(buf)
// Create custom struct pool
type Request struct {
ID int
Data []byte
}
requestPool := bytepool.NewPool(func() *Request {
return &Request{Data: make([]byte, 1024)}
})| Operation | BytePool | Direct Allocation | Performance Gain |
|---|---|---|---|
| Small object allocation (128B) | 88.94 ns/op | 532.5 ns/op | ~6x |
| Large object allocation (64KB) | 761.0 ns/op | - | - |
| Statistics overhead | +11 ns/op | - | Minimal |
Issues and Pull Requests are welcome!
MIT License