Skip to content

Commit feb429d

Browse files
authored
Merge pull request #19 from koss-null/1.0.9
v1.0.9 HUGE PERFORMANCE BOOST: kill per-element allocation in Map (destination-pointer generator) Internal GeneratorFn now writes into a caller-provided slot (func(i int, dst *T) bool) instead of returning a fresh *T. Stages reuse one scratch per worker, so Map stops allocating a heap box per element. 1M elements: Map 1,000,005 -> 6 allocs/op (96 -> 70 ms); MapParallel 1,000,022 -> 25 allocs/op (43.5 -> 32.5 ms). **No exported signature changed.** Behaviour: First/Any now return a pointer to a copy (not into the source);
2 parents 8222983 + 78fdaf6 commit feb429d

27 files changed

Lines changed: 475 additions & 326 deletions

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ These two only exist on `PiperNoLen` and are how you turn an unknown-length pipe
155155

156156
Sometimes your pipeline needs to lug around different types at different steps. `Erase` gives up the compile-time type so you can work with `any`, and `Collect` puts a type back on.
157157

158-
- :frog: `Erase() Piper[any]` (or `PiperNoLen[any]`) -- every element becomes `any` (backed by a pointer to the original value).
158+
- :frog: `Erase() Piper[any]` (or `PiperNoLen[any]`) -- every element becomes `any` (each backed by a `*T`, a pointer to the value).
159159
- :frog: `pipe.Collect[DstT](Piper[any]) Piper[DstT]` -- pull a `Piper[any]` back into a concrete `Piper[DstT]`.
160160
- :frog: `pipe.CollectNL[DstT](PiperNoLen[any]) PiperNoLen[DstT]` -- the same for a pipe with no known length.
161161

‎internal/algo/parallel/qsort/qsort.go‎

Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,13 @@ func qsort[T any](
6262
func partition[T any](data []T, lf, rg int, less func(*T, *T) bool) int {
6363
// small arrays are not sorted with this method
6464
midIdx := (rg-lf)/2 + lf
65-
med, medIdx := median([3]T{data[lf], data[midIdx], data[rg]}, less)
66-
switch medIdx {
67-
case 0:
68-
data[lf], data[midIdx] = data[midIdx], data[lf]
69-
case 2:
70-
data[rg], data[midIdx] = data[midIdx], data[rg]
71-
}
65+
med := median([3]T{data[lf], data[midIdx], data[rg]}, less)
7266

7367
for lf <= rg {
74-
for less(&data[lf], med) {
68+
for less(&data[lf], &med) {
7569
lf++
7670
}
77-
for less(med, &data[rg]) {
71+
for less(&med, &data[rg]) {
7872
rg--
7973
}
8074
if lf >= rg {
@@ -88,37 +82,37 @@ func partition[T any](data []T, lf, rg int, less func(*T, *T) bool) int {
8882
return rg
8983
}
9084

91-
// median returns the median of 3 elements; it's a bit ugly bit effective
92-
func median[T any](elems [3]T, less func(*T, *T) bool) (*T, int16) {
85+
// median returns the median value of 3 elements.
86+
func median[T any](elems [3]T, less func(*T, *T) bool) T {
9387
if less(&elems[1], &elems[0]) && less(&elems[0], &elems[2]) {
94-
return &elems[0], 0
88+
return elems[0]
9589
}
9690
if less(&elems[2], &elems[0]) && less(&elems[0], &elems[1]) {
97-
return &elems[0], 0
91+
return elems[0]
9892
}
9993

10094
if less(&elems[0], &elems[1]) && less(&elems[1], &elems[2]) {
101-
return &elems[1], 1
95+
return elems[1]
10296
}
10397
if less(&elems[2], &elems[1]) && less(&elems[1], &elems[0]) {
104-
return &elems[1], 1
98+
return elems[1]
10599
}
106100

107101
if less(&elems[0], &elems[2]) && less(&elems[2], &elems[1]) {
108-
return &elems[2], 2
102+
return elems[2]
109103
}
110104
if less(&elems[1], &elems[2]) && less(&elems[2], &elems[0]) {
111-
return &elems[2], 2
105+
return elems[2]
112106
}
113107

114108
// two elements are equal
115109
if !less(&elems[0], &elems[1]) && !less(&elems[1], &elems[0]) {
116-
return &elems[0], 0
110+
return elems[0]
117111
}
118112
if !less(&elems[1], &elems[2]) && !less(&elems[2], &elems[1]) {
119-
return &elems[1], 1
113+
return elems[1]
120114
}
121-
return &elems[2], 2
115+
return elems[2]
122116
}
123117

124118
func genTickets(n int) chan struct{} {

‎internal/algo/parallel/qsort/qsort_test.go‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,12 @@ func Test_sort_same(t *testing.T) {
156156
}
157157

158158
func Test_median(t *testing.T) {
159-
val, idx := median([...]int{1, 1, 3}, func(x, y *int) bool { return *x < *y })
160-
require.Equal(t, 1, *val)
161-
require.Equal(t, int16(0), idx)
159+
val := median([...]int{1, 1, 3}, func(x, y *int) bool { return *x < *y })
160+
require.Equal(t, 1, val)
162161

163-
val, idx = median([...]int{0, 1, 1}, func(x, y *int) bool { return *x < *y })
164-
require.Equal(t, 1, *val)
165-
require.Equal(t, int16(1), idx)
162+
val = median([...]int{0, 1, 1}, func(x, y *int) bool { return *x < *y })
163+
require.Equal(t, 1, val)
166164

167-
val, idx = median([...]int{1, 2, 1}, func(x, y *int) bool { return *x < *y })
168-
require.Equal(t, 1, *val)
169-
require.Equal(t, int16(2), idx)
165+
val = median([...]int{1, 2, 1}, func(x, y *int) bool { return *x < *y })
166+
require.Equal(t, 1, val)
170167
}

‎internal/internalpipe/any.go‎

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,36 @@
11
package internalpipe
22

33
import (
4+
"runtime"
45
"sync"
5-
"time"
66
)
77

8+
// hugeLenStep is the base chunk size used when the pipe length is unknown.
89
const hugeLenStep = 1 << 15
910

10-
func anySingleThread[T any](limit int, fn GeneratorFn[T]) *T {
11-
var obj *T
12-
var skipped bool
11+
// anyResCheckInterval controls how often a worker re-reads the shared result
12+
// flag while scanning a chunk, so it does not take a lock on every element.
13+
const anyResCheckInterval = 64
1314

15+
func anySingleThread[T any](limit int, fn GeneratorFn[T]) *T {
16+
var obj T
1417
for i := 0; i < limit; i++ {
15-
if obj, skipped = fn(i); !skipped {
16-
return obj
18+
if !fn(i, &obj) {
19+
return &obj
1720
}
1821
}
1922
return nil
2023
}
2124

2225
// Any returns a pointer to a random element in the pipe or nil if none left.
2326
func (p Pipe[T]) Any() *T {
24-
const mutexUpdateCoef = 18
25-
2627
limit := p.limit()
2728
if p.GoroutinesCnt == 1 {
2829
return anySingleThread(limit, p.Fn)
2930
}
3031

3132
bounded := p.lenSet() || p.limitSet()
32-
step := hugeLenStep
33+
step := hugeLenStep * max(1, runtime.GOMAXPROCS(0))
3334
if bounded {
3435
step = max(divUp(limit, p.GoroutinesCnt), 1)
3536
}
@@ -53,6 +54,12 @@ func (p Pipe[T]) Any() *T {
5354
}
5455
mx.Unlock()
5556
}
57+
getResSet := func() bool {
58+
mx.Lock()
59+
rs := resSet
60+
mx.Unlock()
61+
return rs
62+
}
5663

5764
go func() {
5865
// i >= 0 is for an int owerflow case
@@ -78,43 +85,17 @@ func (p Pipe[T]) Any() *T {
7885
rg = min(rg, limit)
7986
}
8087

81-
var avgFnTime time.Duration
82-
var avgUpdResSetTime time.Duration
83-
resSetUpdCnt := int64(0)
84-
beforeLastResSetUpd := 0
85-
86-
getResSet := func() bool {
87-
start := time.Now()
88-
mx.Lock()
89-
rs := resSet
90-
mx.Unlock()
91-
avgUpdResSetTime = time.Duration(
92-
(int64(time.Since(start)) + int64(avgUpdResSetTime)*(resSetUpdCnt)) / (resSetUpdCnt + 1),
93-
)
94-
resSetUpdCnt++
95-
beforeLastResSetUpd = 0
96-
return rs
88+
if getResSet() {
89+
return
9790
}
98-
rs := getResSet()
99-
cnt := 0
91+
var obj T
10092
for j := lf; j < rg; j++ {
101-
beforeLastResSetUpd++
102-
if j != lf &&
103-
avgFnTime != 0 &&
104-
int64(beforeLastResSetUpd) > (mutexUpdateCoef*int64(avgUpdResSetTime)/int64(avgFnTime)) {
105-
rs = getResSet()
106-
cnt++
93+
if j != lf && (j-lf)%anyResCheckInterval == 0 && getResSet() {
94+
return
10795
}
108-
if !rs {
109-
start := time.Now()
110-
obj, skipped := p.Fn(j)
111-
if !skipped {
112-
setObj(obj)
113-
return
114-
}
115-
avgFnTime = time.Duration(
116-
(int64(time.Since(start)) + int64(avgFnTime)*int64(j-lf)) / int64(j-lf+1),
117-
)
96+
if !p.Fn(j, &obj) {
97+
setObj(&obj)
98+
return
11899
}
119100
}
120101
}(i, i+step)

‎internal/internalpipe/constructor.go‎

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@ const (
1313

1414
func Slice[T any](dt []T) Pipe[T] {
1515
return Pipe[T]{
16-
Fn: func(i int) (*T, bool) {
16+
Fn: func(i int, dst *T) bool {
1717
if i >= len(dt) {
18-
return nil, true
18+
return true
1919
}
20-
return &dt[i], false
20+
*dst = dt[i]
21+
return false
2122
},
2223
Len: len(dt),
2324
ValLim: notSet,
@@ -27,9 +28,13 @@ func Slice[T any](dt []T) Pipe[T] {
2728

2829
func Func[T any](fn func(i int) (T, bool)) Pipe[T] {
2930
return Pipe[T]{
30-
Fn: func(i int) (*T, bool) {
31+
Fn: func(i int, dst *T) bool {
3132
obj, exist := fn(i)
32-
return &obj, !exist
33+
if !exist {
34+
return true
35+
}
36+
*dst = obj
37+
return false
3338
},
3439
Len: notSet,
3540
ValLim: notSet,
@@ -39,9 +44,13 @@ func Func[T any](fn func(i int) (T, bool)) Pipe[T] {
3944

4045
func FuncP[T any](fn func(i int) (*T, bool)) Pipe[T] {
4146
return Pipe[T]{
42-
Fn: func(i int) (*T, bool) {
47+
Fn: func(i int, dst *T) bool {
4348
obj, exist := fn(i)
44-
return obj, !exist
49+
if !exist {
50+
return true
51+
}
52+
*dst = *obj
53+
return false
4554
},
4655
Len: notSet,
4756
ValLim: notSet,
@@ -82,9 +91,10 @@ func Range[T constraints.Integer | constraints.Float](start, finish, step T) Pip
8291
}
8392
}
8493
return Pipe[T]{
85-
Fn: func(i int) (*T, bool) {
94+
Fn: func(i int, dst *T) bool {
8695
val := start + T(i)*step
87-
return &val, pred(val)
96+
*dst = val
97+
return pred(val)
8898
},
8999
Len: rangeLen(start, finish, step, pred),
90100
ValLim: notSet,
@@ -103,9 +113,9 @@ func Repeat[T any](x T, n int) Pipe[T] {
103113
}
104114

105115
return Pipe[T]{
106-
Fn: func(i int) (*T, bool) {
107-
cp := x
108-
return &cp, i >= n
116+
Fn: func(i int, dst *T) bool {
117+
*dst = x
118+
return i >= n
109119
},
110120
Len: n,
111121
ValLim: notSet,

‎internal/internalpipe/constructor_test.go‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ func Test_Slice(t *testing.T) {
1515
require.Equal(t, p.Len, 10)
1616
require.Equal(t, p.ValLim, notSet)
1717
require.Equal(t, p.GoroutinesCnt, defaultParallelWrks)
18-
r, _ := p.Fn(5)
19-
require.Equal(t, *r, a[5])
20-
_, skipped := p.Fn(15)
21-
require.True(t, skipped)
18+
var r int
19+
require.True(t, p.Fn(15, &r))
20+
require.False(t, p.Fn(5, &r))
21+
require.Equal(t, a[5], r)
2222
}
2323

2424
func Test_FuncP(t *testing.T) {
@@ -163,8 +163,9 @@ func Test_Range_FloatLen(t *testing.T) {
163163
p := Range(c.start, c.finish, c.step)
164164

165165
yielded := 0
166+
var tmp float64
166167
for i := 0; ; i++ {
167-
if _, skipped := p.Fn(i); skipped {
168+
if p.Fn(i, &tmp) {
168169
break
169170
}
170171
yielded++

0 commit comments

Comments
 (0)