Skip to content

Commit 98ab56c

Browse files
committed
lib: added zeropool library
Signed-off-by: Anagh Kumar Baranwal <6824881+darthShadow@users.noreply.github.com>
1 parent e6991d6 commit 98ab56c

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed

lib/zeropool/pool.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Package zeropool provides a zero-allocation type-safe alternative for sync.Pool, used to workaround staticheck SA6002.
2+
// The contents of this package are brought from https://github.com/colega/zeropool because "little copying is better than little dependency".
3+
package zeropool
4+
5+
import (
6+
"sync"
7+
)
8+
9+
// Pool is a type-safe pool of items that does not allocate pointers to items.
10+
// That is not entirely true, it does allocate sometimes, but not most of the time,
11+
// just like the usual sync.Pool pools items most of the time, except when they're evicted.
12+
// It does that by storing the allocated pointers in a secondary pool instead of letting them go,
13+
// so they can be used later to store the items again.
14+
//
15+
// Zero value of Pool[T] is valid, and it will return zero values of T if nothing is pooled.
16+
type Pool[T any] struct {
17+
// items holds pointers to the pooled items, which are valid to be used.
18+
items sync.Pool
19+
// pointers holds just pointers to the pooled item types.
20+
// The values referenced by pointers are not valid to be used (as they're used by some other caller)
21+
// and it is safe to overwrite these pointers.
22+
pointers sync.Pool
23+
}
24+
25+
// New creates a new Pool[T] with the given function to create new items.
26+
// A Pool must not be copied after first use.
27+
func New[T any](item func() T) Pool[T] {
28+
return Pool[T]{
29+
items: sync.Pool{
30+
New: func() interface{} {
31+
val := item()
32+
return &val
33+
},
34+
},
35+
}
36+
}
37+
38+
// Get returns an item from the pool, creating a new one if necessary.
39+
// Get may be called concurrently from multiple goroutines.
40+
func (p *Pool[T]) Get() T {
41+
pooled := p.items.Get()
42+
if pooled == nil {
43+
// The only way this can happen is when someone is using the zero-value of zeropool.Pool, and items pool is empty.
44+
// We don't have a pointer to store in p.pointers, so just return the empty value.
45+
var zero T
46+
return zero
47+
}
48+
49+
ptr := pooled.(*T)
50+
item := *ptr
51+
var zero T
52+
// We don't want to retain the value in p.pointers.
53+
// If T holds a reference to something, we want that to be garbage-collected
54+
// if for some reason caller does less Put() calls than Get() calls.
55+
*ptr = zero
56+
p.pointers.Put(ptr)
57+
return item
58+
}
59+
60+
// Put adds an item to the pool.
61+
func (p *Pool[T]) Put(item T) {
62+
var ptr *T
63+
if pooled := p.pointers.Get(); pooled != nil {
64+
ptr = pooled.(*T)
65+
} else {
66+
ptr = new(T)
67+
}
68+
*ptr = item
69+
p.items.Put(ptr)
70+
}

lib/zeropool/pool_test.go

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package zeropool_test
2+
3+
import (
4+
"math"
5+
"reflect"
6+
"sync"
7+
"sync/atomic"
8+
"testing"
9+
10+
"github.com/rclone/rclone/lib/zeropool"
11+
)
12+
13+
func TestPool(t *testing.T) {
14+
t.Run("provides correct values", func(t *testing.T) {
15+
pool := zeropool.New(func() []byte { return make([]byte, 1024) })
16+
item1 := pool.Get()
17+
assertEqual(t, 1024, len(item1))
18+
19+
item2 := pool.Get()
20+
assertEqual(t, 1024, len(item2))
21+
22+
pool.Put(item1)
23+
pool.Put(item2)
24+
25+
item1 = pool.Get()
26+
assertEqual(t, 1024, len(item1))
27+
28+
item2 = pool.Get()
29+
assertEqual(t, 1024, len(item2))
30+
})
31+
32+
t.Run("is not racy", func(t *testing.T) {
33+
pool := zeropool.New(func() []byte { return make([]byte, 1024) })
34+
35+
const iterations = 1e6
36+
const concurrency = math.MaxUint8
37+
var counter atomic.Int64
38+
39+
do := make(chan struct{}, 1e6)
40+
for i := 0; i < iterations; i++ {
41+
do <- struct{}{}
42+
}
43+
close(do)
44+
45+
run := make(chan struct{})
46+
done := sync.WaitGroup{}
47+
done.Add(concurrency)
48+
for i := 0; i < concurrency; i++ {
49+
go func(worker int) {
50+
<-run
51+
for range do {
52+
item := pool.Get()
53+
item[0] = byte(worker)
54+
counter.Add(1) // Counts and also adds some delay to add raciness.
55+
if item[0] != byte(worker) {
56+
panic("wrong value")
57+
}
58+
pool.Put(item)
59+
}
60+
done.Done()
61+
}(i)
62+
}
63+
close(run)
64+
done.Wait()
65+
t.Logf("Done %d iterations", counter.Load())
66+
})
67+
68+
t.Run("does not allocate", func(t *testing.T) {
69+
pool := zeropool.New(func() []byte { return make([]byte, 1024) })
70+
// Warm up, this will alloate one slice.
71+
slice := pool.Get()
72+
pool.Put(slice)
73+
74+
allocs := testing.AllocsPerRun(1000, func() {
75+
slice := pool.Get()
76+
pool.Put(slice)
77+
})
78+
assertEqualf(t, float64(0), allocs, "Should not allocate.")
79+
})
80+
81+
t.Run("zero value is valid", func(t *testing.T) {
82+
var pool zeropool.Pool[[]byte]
83+
slice := pool.Get()
84+
pool.Put(slice)
85+
86+
allocs := testing.AllocsPerRun(1000, func() {
87+
slice := pool.Get()
88+
pool.Put(slice)
89+
})
90+
assertEqualf(t, float64(0), allocs, "Should not allocate.")
91+
})
92+
}
93+
94+
func BenchmarkZeropoolPool(b *testing.B) {
95+
b.Run("same goroutine", func(b *testing.B) {
96+
pool := zeropool.New(func() []byte { return make([]byte, 1024) })
97+
98+
// Warmup
99+
item := pool.Get()
100+
pool.Put(item)
101+
102+
b.ResetTimer()
103+
for i := 0; i < b.N; i++ {
104+
item := pool.Get()
105+
pool.Put(item)
106+
}
107+
})
108+
109+
b.Run("different goroutines", func(b *testing.B) {
110+
pool := zeropool.New(func() []byte { return make([]byte, 1024) })
111+
112+
ch := make(chan []byte)
113+
go func() {
114+
for item := range ch {
115+
pool.Put(item)
116+
}
117+
}()
118+
defer close(ch)
119+
120+
// Warmup.
121+
ch <- pool.Get()
122+
123+
b.ResetTimer()
124+
for i := 0; i < b.N; i++ {
125+
ch <- pool.Get()
126+
}
127+
})
128+
129+
}
130+
131+
// BenchmarkSyncPoolValue uses sync.Pool to store values, which makes an allocation on each Put call.
132+
func BenchmarkSyncPoolValue(b *testing.B) {
133+
b.Run("same goroutine", func(b *testing.B) {
134+
pool := sync.Pool{New: func() any {
135+
return make([]byte, 1024)
136+
}}
137+
138+
// Warmup.
139+
item := pool.Get().([]byte)
140+
pool.Put(item) //nolint:staticcheck // This allocates.
141+
142+
b.ResetTimer()
143+
for i := 0; i < b.N; i++ {
144+
item := pool.Get().([]byte)
145+
pool.Put(item) //nolint:staticcheck // This allocates.
146+
}
147+
})
148+
149+
b.Run("different goroutines", func(b *testing.B) {
150+
pool := sync.Pool{New: func() any {
151+
return make([]byte, 1024)
152+
}}
153+
154+
ch := make(chan []byte)
155+
go func() {
156+
for item := range ch {
157+
pool.Put(item) //nolint:staticcheck // This allocates.
158+
}
159+
}()
160+
defer close(ch)
161+
162+
// Warmup
163+
ch <- pool.Get().([]byte)
164+
165+
b.ResetTimer()
166+
for i := 0; i < b.N; i++ {
167+
ch <- pool.Get().([]byte)
168+
}
169+
})
170+
}
171+
172+
// BenchmarkSyncPoolNewPointer uses sync.Pool to store pointers, but it calls Put with a new pointer every time.
173+
func BenchmarkSyncPoolNewPointer(b *testing.B) {
174+
b.Run("same goroutine", func(b *testing.B) {
175+
pool := sync.Pool{New: func() any {
176+
v := make([]byte, 1024)
177+
return &v
178+
}}
179+
180+
// Warmup
181+
item := pool.Get().(*[]byte)
182+
pool.Put(item)
183+
184+
b.ResetTimer()
185+
for i := 0; i < b.N; i++ {
186+
item := pool.Get().(*[]byte)
187+
buf := *item
188+
pool.Put(&buf) //nolint:staticcheck // New pointer.
189+
}
190+
})
191+
192+
b.Run("different goroutines", func(b *testing.B) {
193+
pool := sync.Pool{New: func() any {
194+
v := make([]byte, 1024)
195+
return &v
196+
}}
197+
ch := make(chan []byte)
198+
go func() {
199+
for item := range ch {
200+
pool.Put(&item) //nolint:staticcheck // New pointer.
201+
}
202+
}()
203+
defer close(ch)
204+
205+
// Warmup
206+
ch <- *(pool.Get().(*[]byte))
207+
208+
b.ResetTimer()
209+
for i := 0; i < b.N; i++ {
210+
ch <- *(pool.Get().(*[]byte))
211+
}
212+
})
213+
}
214+
215+
// BenchmarkSyncPoolPointer illustrates the optimal usage of sync.Pool, not always possible.
216+
func BenchmarkSyncPoolPointer(b *testing.B) {
217+
b.Run("same goroutine", func(b *testing.B) {
218+
pool := sync.Pool{New: func() any {
219+
v := make([]byte, 1024)
220+
return &v
221+
}}
222+
223+
// Warmup
224+
item := pool.Get().(*[]byte)
225+
pool.Put(item)
226+
227+
b.ResetTimer()
228+
for i := 0; i < b.N; i++ {
229+
item := pool.Get().(*[]byte)
230+
pool.Put(item)
231+
}
232+
})
233+
234+
b.Run("different goroutines", func(b *testing.B) {
235+
pool := sync.Pool{New: func() any {
236+
v := make([]byte, 1024)
237+
return &v
238+
}}
239+
240+
ch := make(chan *[]byte)
241+
go func() {
242+
for item := range ch {
243+
pool.Put(item)
244+
}
245+
}()
246+
defer close(ch)
247+
248+
// Warmup
249+
ch <- pool.Get().(*[]byte)
250+
251+
b.ResetTimer()
252+
for i := 0; i < b.N; i++ {
253+
ch <- pool.Get().(*[]byte)
254+
}
255+
})
256+
}
257+
258+
func assertEqual(t *testing.T, expected, got interface{}) {
259+
t.Helper()
260+
if !reflect.DeepEqual(expected, got) {
261+
t.Logf("Expected %v, got %v", expected, got)
262+
t.Fail()
263+
}
264+
}
265+
266+
func assertEqualf(t *testing.T, expected, got interface{}, msg string, args ...any) {
267+
t.Helper()
268+
if !reflect.DeepEqual(expected, got) {
269+
t.Logf("Expected %v, got %v", expected, got)
270+
t.Errorf(msg, args...)
271+
}
272+
}

0 commit comments

Comments
 (0)