package utils
import (
"reflect"
"sync/atomic"
"unsafe"
)
// BytePool is used to concurrently obtain byte arrays of size cap.
type BytePool struct {
i atomic.Int64
caches [][]byte
used []atomic.Bool
head uintptr
cap int
}
// NewBytePool creates a byte array pool with max byte arrays, each with a capacity of cap.
// If sync.Pool is used, at least 24 bytes of memory will be allocated each time,
// see https://blog.mike.norgate.xyz/unlocking-go-slice-performance-navigating-sync-pool-for-enhanced-efficiency-7cb63b0b453e.
// By using the space-for-time method, zero memory allocation can be achieved.
// Here, a large byte array is first allocated, and then it is divided into small byte arrays.
// The address difference between the small byte arrays is the same.
// For each byte array, an atomic.Bool is used to determine whether it is in use.
// If it is false, it means it is not in use, and it is converted to true and returned to the caller;
// if it is true, it means it has been used.
// When the byte array is used,
// we find the corresponding atomic.Bool by the difference between its address and the first address of the large byte array,
// and then set it to false.
func NewBytePool(cap, max int) *BytePool {
data := make([]byte, cap*max)
caches := make([][]byte, max)
for i := range caches {
caches[i] = data[i*cap : (i+1)*cap][:0]
}
rt := (*reflect.SliceHeader)(unsafe.Pointer(&data))
return &BytePool{
caches: caches,
used: make([]atomic.Bool, max),
head: rt.Data,
cap: cap,
}
}
// Get byte array from pool.
func (b *BytePool) Get() []byte {
for {
cur := b.i.Add(1)
if cur >= int64(len(b.caches)) {
cur = 0
b.i.Store(0)
}
if b.used[cur].CompareAndSwap(false, true) {
return b.caches[cur][:0]
}
}
}
// Put the data back into the BytePool.
func (b *BytePool) Put(x []byte) {
rt := (*reflect.SliceHeader)(unsafe.Pointer(&x))
i := int(rt.Data-b.head) / b.cap
b.used[i].Store(false)
}