Skip to content

Commit d3952e5

Browse files
committed
use golang 1.19 native atomic types
1 parent 0bf4046 commit d3952e5

File tree

4 files changed

+58
-53
lines changed

4 files changed

+58
-53
lines changed

Diff for: README.md

+6-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Benchmarks to support the above claims [here](#benchmarks)
88

99
## Installation
1010

11-
You need Golang [1.18.x](https://go.dev/dl/) or above since this package uses generics
11+
You need Golang [1.19.x](https://go.dev/dl/) or above
1212

1313
```bash
1414
$ go get github.com/alphadose/[email protected]
@@ -101,12 +101,12 @@ OS -> darwin
101101
Results were computed from [benchstat](https://pkg.go.dev/golang.org/x/perf/cmd/benchstat) of 30 cases
102102
```
103103
name time/op
104-
UnlimitedGoroutines-8 301ms ± 4%
104+
UnlimitedGoroutines-8 331ms ± 4%
105105
ErrGroup-8 515ms ± 9%
106106
AntsPool-8 582ms ± 9%
107107
GammaZeroPool-8 740ms ±13%
108108
BytedanceGoPool-8 572ms ±18%
109-
ItogamiPool-8 331ms ± 7%
109+
ItogamiPool-8 337ms ± 1%
110110
111111
name alloc/op
112112
UnlimitedGoroutines-8 96.3MB ± 0%
@@ -120,14 +120,14 @@ name allocs/op
120120
UnlimitedGoroutines-8 2.00M ± 0%
121121
ErrGroup-8 3.00M ± 0%
122122
AntsPool-8 1.10M ± 2%
123-
GammaZeroPool-8 1.05M ± 0%
123+
GammaZeroPool-8 1.08M ± 0%
124124
BytedanceGoPool-8 2.59M ± 1%
125-
ItogamiPool-8 1.05M ± 0%
125+
ItogamiPool-8 1.08M ± 0%
126126
```
127127

128128
The following conclusions can be drawn from the above results:-
129129

130-
1. [Itogami](https://github.com/alphadose/itogami) is the fastest among all goroutine pool implementations and slower only than unlimited goroutines
130+
1. [Itogami](https://github.com/alphadose/itogami) is the fastest among all goroutine pool implementations and slightly slower than unlimited goroutines
131131
2. [Itogami](https://github.com/alphadose/itogami) has the least `allocs/op` and hence the memory usage scales really well with high load
132132
3. The memory used per operation is in the acceptable range of other pools and drastically lower than unlimited goroutines
133133
4. The tolerance (± %) for [Itogami](https://github.com/alphadose/itogami) is quite low for all 3 metrics indicating that the algorithm is quite stable overall

Diff for: go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module github.com/alphadose/itogami
22

3-
go 1.18
3+
go 1.19

Diff for: pool.go

+15-14
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ type Pool struct {
1919
maxSize uint64
2020
_p2 [cacheLinePadSize - unsafe.Sizeof(uint64(0))]byte
2121
// using a stack keeps cpu caches warm based on FILO property
22-
top unsafe.Pointer
23-
_p3 [cacheLinePadSize - unsafe.Sizeof(unsafe.Pointer(nil))]byte
22+
top atomic.Pointer[node]
23+
_p3 [cacheLinePadSize - unsafe.Sizeof(atomic.Pointer[node]{})]byte
2424
}
2525

2626
// NewPool returns a new thread pool
@@ -78,23 +78,24 @@ var (
7878

7979
// a single node in this stack
8080
type node struct {
81-
next unsafe.Pointer
81+
next atomic.Pointer[node]
8282
value *slot
8383
}
8484

8585
// pop pops value from the top of the stack
8686
func (self *Pool) pop() (value *slot) {
87-
var top, next unsafe.Pointer
87+
var top, next *node
8888
for {
89-
top = atomic.LoadPointer(&self.top)
89+
top = self.top.Load()
9090
if top == nil {
9191
return
9292
}
93-
next = atomic.LoadPointer(&(*node)(top).next)
94-
if atomic.CompareAndSwapPointer(&self.top, top, next) {
95-
value = (*node)(top).value
96-
(*node)(top).next, (*node)(top).value = nil, nil
97-
itemFree((*node)(top))
93+
next = top.next.Load()
94+
if self.top.CompareAndSwap(top, next) {
95+
value = top.value
96+
top.value = nil
97+
top.next.Store(nil)
98+
itemFree(top)
9899
return
99100
}
100101
}
@@ -103,14 +104,14 @@ func (self *Pool) pop() (value *slot) {
103104
// push pushes a value on top of the stack
104105
func (self *Pool) push(v *slot) {
105106
var (
106-
top unsafe.Pointer
107+
top *node
107108
item = itemAlloc().(*node)
108109
)
109110
item.value = v
110111
for {
111-
top = atomic.LoadPointer(&self.top)
112-
item.next = top
113-
if atomic.CompareAndSwapPointer(&self.top, top, unsafe.Pointer(item)) {
112+
top = self.top.Load()
113+
item.next.Store(top)
114+
if self.top.CompareAndSwap(top, item) {
114115
return
115116
}
116117
}

Diff for: pool_func.go

+36-32
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,30 @@ import (
66
"unsafe"
77
)
88

9-
// a single slot for a worker in PoolWithFunc
10-
type slotFunc[T any] struct {
11-
threadPtr unsafe.Pointer
12-
data T
13-
}
9+
type (
10+
// a single slot for a worker in PoolWithFunc
11+
slotFunc[T any] struct {
12+
threadPtr unsafe.Pointer
13+
data T
14+
}
1415

15-
// PoolWithFunc is used for spawning workers for a single pre-defined function with myriad inputs
16-
// useful for throughput bound cases
17-
// has lower memory usage and allocs per op than the default Pool
18-
// ( type -> func(T) {} ) where T is a generic parameter
19-
type PoolWithFunc[T any] struct {
20-
currSize uint64
21-
_p1 [cacheLinePadSize - unsafe.Sizeof(uint64(0))]byte
22-
maxSize uint64
23-
alloc func() any
24-
free func(any)
25-
task func(T)
26-
_p2 [cacheLinePadSize - unsafe.Sizeof(uint64(0)) - 3*unsafe.Sizeof(func() {})]byte
27-
top unsafe.Pointer
28-
_p3 [cacheLinePadSize - unsafe.Sizeof(unsafe.Pointer(nil))]byte
29-
}
16+
// PoolWithFunc is used for spawning workers for a single pre-defined function with myriad inputs
17+
// useful for throughput bound cases
18+
// has lower memory usage and allocs per op than the default Pool
19+
//
20+
// ( type -> func(T) {} ) where T is a generic parameter
21+
PoolWithFunc[T any] struct {
22+
currSize uint64
23+
_p1 [cacheLinePadSize - unsafe.Sizeof(uint64(0))]byte
24+
maxSize uint64
25+
alloc func() any
26+
free func(any)
27+
task func(T)
28+
_p2 [cacheLinePadSize - unsafe.Sizeof(uint64(0)) - 3*unsafe.Sizeof(func() {})]byte
29+
top atomic.Pointer[dataItem[T]]
30+
_p3 [cacheLinePadSize - unsafe.Sizeof(atomic.Pointer[dataItem[T]]{})]byte
31+
}
32+
)
3033

3134
// NewPoolWithFunc returns a new PoolWithFunc
3235
func NewPoolWithFunc[T any](size uint64, task func(T)) *PoolWithFunc[T] {
@@ -68,23 +71,24 @@ func (self *PoolWithFunc[T]) loopQ(d *slotFunc[T]) {
6871

6972
// a single node in the stack
7073
type dataItem[T any] struct {
71-
next unsafe.Pointer
74+
next atomic.Pointer[dataItem[T]]
7275
value *slotFunc[T]
7376
}
7477

7578
// pop pops value from the top of the stack
7679
func (self *PoolWithFunc[T]) pop() (value *slotFunc[T]) {
77-
var top, next unsafe.Pointer
80+
var top, next *dataItem[T]
7881
for {
79-
top = atomic.LoadPointer(&self.top)
82+
top = self.top.Load()
8083
if top == nil {
8184
return
8285
}
83-
next = atomic.LoadPointer(&(*dataItem[T])(top).next)
84-
if atomic.CompareAndSwapPointer(&self.top, top, next) {
85-
value = (*dataItem[T])(top).value
86-
(*dataItem[T])(top).next, (*dataItem[T])(top).value = nil, nil
87-
self.free((*dataItem[T])(top))
86+
next = top.next.Load()
87+
if self.top.CompareAndSwap(top, next) {
88+
value = top.value
89+
top.value = nil
90+
top.next.Store(nil)
91+
self.free(top)
8892
return
8993
}
9094
}
@@ -93,14 +97,14 @@ func (self *PoolWithFunc[T]) pop() (value *slotFunc[T]) {
9397
// push pushes a value on top of the stack
9498
func (self *PoolWithFunc[T]) push(v *slotFunc[T]) {
9599
var (
96-
top unsafe.Pointer
100+
top *dataItem[T]
97101
item = self.alloc().(*dataItem[T])
98102
)
99103
item.value = v
100104
for {
101-
top = atomic.LoadPointer(&self.top)
102-
item.next = top
103-
if atomic.CompareAndSwapPointer(&self.top, top, unsafe.Pointer(item)) {
105+
top = self.top.Load()
106+
item.next.Store(top)
107+
if self.top.CompareAndSwap(top, item) {
104108
return
105109
}
106110
}

0 commit comments

Comments
 (0)