Skip to content

Commit f7eb7fd

Browse files
authored
Merge branch 'master' into perf/scope-pooling
2 parents d557072 + a9ac10f commit f7eb7fd

File tree

3 files changed

+291
-6
lines changed

3 files changed

+291
-6
lines changed

bench_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,3 +571,71 @@ func Benchmark_reduce(b *testing.B) {
571571

572572
require.Equal(b, 5050, out.(int))
573573
}
574+
575+
func Benchmark_min(b *testing.B) {
576+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
577+
env := map[string]any{"arr": arr}
578+
579+
program, err := expr.Compile(`min(arr)`, expr.Env(env))
580+
require.NoError(b, err)
581+
582+
var out any
583+
b.ResetTimer()
584+
for n := 0; n < b.N; n++ {
585+
out, _ = vm.Run(program, env)
586+
}
587+
b.StopTimer()
588+
589+
require.Equal(b, 1, out)
590+
}
591+
592+
func Benchmark_max(b *testing.B) {
593+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
594+
env := map[string]any{"arr": arr}
595+
596+
program, err := expr.Compile(`max(arr)`, expr.Env(env))
597+
require.NoError(b, err)
598+
599+
var out any
600+
b.ResetTimer()
601+
for n := 0; n < b.N; n++ {
602+
out, _ = vm.Run(program, env)
603+
}
604+
b.StopTimer()
605+
606+
require.Equal(b, 100, out)
607+
}
608+
609+
func Benchmark_mean(b *testing.B) {
610+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
611+
env := map[string]any{"arr": arr}
612+
613+
program, err := expr.Compile(`mean(arr)`, expr.Env(env))
614+
require.NoError(b, err)
615+
616+
var out any
617+
b.ResetTimer()
618+
for n := 0; n < b.N; n++ {
619+
out, _ = vm.Run(program, env)
620+
}
621+
b.StopTimer()
622+
623+
require.Equal(b, 50.5, out)
624+
}
625+
626+
func Benchmark_median(b *testing.B) {
627+
arr := []any{55, 58, 42, 61, 75, 52, 64, 62, 16, 79, 40, 14, 50, 76, 23, 2, 5, 80, 89, 51, 21, 96, 91, 13, 71, 82, 65, 63, 11, 17, 94, 81, 74, 4, 97, 1, 39, 3, 28, 8, 84, 90, 47, 85, 7, 56, 49, 93, 33, 12, 19, 60, 86, 100, 44, 45, 36, 72, 95, 77, 34, 92, 24, 73, 18, 38, 43, 26, 41, 69, 67, 57, 9, 27, 66, 87, 46, 35, 59, 70, 10, 20, 53, 15, 32, 98, 68, 31, 54, 25, 83, 88, 22, 48, 29, 37, 6, 78, 99, 30}
628+
env := map[string]any{"arr": arr}
629+
630+
program, err := expr.Compile(`median(arr)`, expr.Env(env))
631+
require.NoError(b, err)
632+
633+
var out any
634+
b.ResetTimer()
635+
for n := 0; n < b.N; n++ {
636+
out, _ = vm.Run(program, env)
637+
}
638+
b.StopTimer()
639+
640+
require.Equal(b, 50.5, out)
641+
}

builtin/builtin_test.go

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,19 @@ import (
2121
func TestBuiltin(t *testing.T) {
2222
ArrayWithNil := []any{42}
2323
env := map[string]any{
24-
"ArrayOfString": []string{"foo", "bar", "baz"},
25-
"ArrayOfInt": []int{1, 2, 3},
26-
"ArrayOfAny": []any{1, "2", true},
27-
"ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}},
28-
"PtrArrayWithNil": &ArrayWithNil,
24+
"ArrayOfString": []string{"foo", "bar", "baz"},
25+
"ArrayOfInt": []int{1, 2, 3},
26+
"ArrayOfFloat": []float64{1.5, 2.5, 3.5},
27+
"ArrayOfInt32": []int32{1, 2, 3},
28+
"ArrayOfAny": []any{1, "2", true},
29+
"ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}},
30+
"PtrArrayWithNil": &ArrayWithNil,
31+
"EmptyIntArray": []int{},
32+
"EmptyFloatArray": []float64{},
33+
"NestedIntArrays": []any{[]int{1, 2}, []int{3, 4}},
34+
"NestedAnyArrays": []any{[]any{1, 2}, []any{3, 4}},
35+
"MixedNestedArray": []any{1, []int{2, 3}, []float64{4.0, 5.0}},
36+
"NestedInt32Array": []any{[]int32{1, 2}, []int32{3, 4}},
2937
}
3038

3139
var tests = []struct {
@@ -86,6 +94,22 @@ func TestBuiltin(t *testing.T) {
8694
{`min([1, 2, 3])`, 1},
8795
{`min([1.5, 2.5, 3.5])`, 1.5},
8896
{`min(-1, [1.5, 2.5, 3.5])`, -1},
97+
{`max(ArrayOfInt)`, 3},
98+
{`min(ArrayOfInt)`, 1},
99+
{`max(ArrayOfFloat)`, 3.5},
100+
{`min(ArrayOfFloat)`, 1.5},
101+
{`max(EmptyIntArray, 5)`, 5},
102+
{`min(EmptyFloatArray, 5)`, 5},
103+
{`max(NestedIntArrays)`, 4},
104+
{`min(NestedIntArrays)`, 1},
105+
{`max(NestedAnyArrays)`, 4},
106+
{`min(NestedAnyArrays)`, 1},
107+
{`max(MixedNestedArray)`, 5.0},
108+
{`min(MixedNestedArray)`, 1},
109+
{`max(ArrayOfInt32)`, int32(3)},
110+
{`min(ArrayOfInt32)`, int32(1)},
111+
{`max(NestedInt32Array)`, int32(4)},
112+
{`min(NestedInt32Array)`, int32(1)},
89113
{`sum(1..9)`, 45},
90114
{`sum([.5, 1.5, 2.5])`, 4.5},
91115
{`sum([])`, 0},
@@ -97,6 +121,13 @@ func TestBuiltin(t *testing.T) {
97121
{`mean(10, [1, 2, 3], 1..9)`, 4.6923076923076925},
98122
{`mean(-10, [1, 2, 3, 4])`, 0.0},
99123
{`mean(10.9, 1..9)`, 5.59},
124+
{`mean(ArrayOfInt)`, 2.0},
125+
{`mean(ArrayOfFloat)`, 2.5},
126+
{`mean(NestedIntArrays)`, 2.5},
127+
{`mean(NestedAnyArrays)`, 2.5},
128+
{`mean(MixedNestedArray)`, 3.0},
129+
{`mean(ArrayOfInt32)`, 2.0},
130+
{`mean(NestedInt32Array)`, 2.5},
100131
{`median(1..9)`, 5.0},
101132
{`median([.5, 1.5, 2.5])`, 1.5},
102133
{`median([])`, 0.0},
@@ -105,6 +136,13 @@ func TestBuiltin(t *testing.T) {
105136
{`median(10, [1, 2, 3], 1..9)`, 4.0},
106137
{`median(-10, [1, 2, 3, 4])`, 2.0},
107138
{`median(1..5, 4.9)`, 3.5},
139+
{`median(ArrayOfInt)`, 2.0},
140+
{`median(ArrayOfFloat)`, 2.5},
141+
{`median(NestedIntArrays)`, 2.5},
142+
{`median(NestedAnyArrays)`, 2.5},
143+
{`median(MixedNestedArray)`, 3.0},
144+
{`median(ArrayOfInt32)`, 2.0},
145+
{`median(NestedInt32Array)`, 2.5},
108146
{`toJSON({foo: 1, bar: 2})`, "{\n \"bar\": 2,\n \"foo\": 1\n}"},
109147
{`fromJSON("[1, 2, 3]")`, []any{1.0, 2.0, 3.0}},
110148
{`toBase64("hello")`, "aGVsbG8="},

builtin/lib.go

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,75 @@ func minMax(name string, fn func(any, any) bool, depth int, args ...any) (any, e
259259
}
260260
var val any
261261
for _, arg := range args {
262+
// Fast paths for common typed slices - avoid reflection and allocations
263+
switch arr := arg.(type) {
264+
case []int:
265+
if len(arr) == 0 {
266+
continue
267+
}
268+
m := arr[0]
269+
for i := 1; i < len(arr); i++ {
270+
if fn(m, arr[i]) {
271+
m = arr[i]
272+
}
273+
}
274+
if val == nil || fn(val, m) {
275+
val = m
276+
}
277+
continue
278+
case []float64:
279+
if len(arr) == 0 {
280+
continue
281+
}
282+
m := arr[0]
283+
for i := 1; i < len(arr); i++ {
284+
if fn(m, arr[i]) {
285+
m = arr[i]
286+
}
287+
}
288+
if val == nil || fn(val, m) {
289+
val = m
290+
}
291+
continue
292+
case []any:
293+
// Fast path for []any with simple numeric types
294+
for _, elem := range arr {
295+
switch e := elem.(type) {
296+
case int, int8, int16, int32, int64,
297+
uint, uint8, uint16, uint32, uint64,
298+
float32, float64:
299+
if val == nil || fn(val, e) {
300+
val = e
301+
}
302+
case []int, []float64, []any:
303+
// Nested array - recurse
304+
nested, err := minMax(name, fn, depth+1, e)
305+
if err != nil {
306+
return nil, err
307+
}
308+
if nested != nil && (val == nil || fn(val, nested)) {
309+
val = nested
310+
}
311+
default:
312+
// Could be another slice type, use reflection
313+
rv := reflect.ValueOf(e)
314+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
315+
nested, err := minMax(name, fn, depth+1, e)
316+
if err != nil {
317+
return nil, err
318+
}
319+
if nested != nil && (val == nil || fn(val, nested)) {
320+
val = nested
321+
}
322+
} else {
323+
return nil, fmt.Errorf("invalid argument for %s (type %T)", name, e)
324+
}
325+
}
326+
}
327+
continue
328+
}
329+
330+
// Slow path: use reflection for other types
262331
rv := reflect.ValueOf(arg)
263332
switch rv.Kind() {
264333
case reflect.Array, reflect.Slice:
@@ -278,7 +347,6 @@ func minMax(name string, fn func(any, any) bool, depth int, args ...any) (any, e
278347
default:
279348
return nil, fmt.Errorf("invalid argument for %s (type %T)", name, elemVal)
280349
}
281-
282350
}
283351
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
284352
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
@@ -305,6 +373,67 @@ func mean(depth int, args ...any) (int, float64, error) {
305373
var count int
306374

307375
for _, arg := range args {
376+
// Fast paths for common typed slices - avoid reflection and allocations
377+
switch arr := arg.(type) {
378+
case []int:
379+
for _, v := range arr {
380+
total += float64(v)
381+
}
382+
count += len(arr)
383+
continue
384+
case []float64:
385+
for _, v := range arr {
386+
total += v
387+
}
388+
count += len(arr)
389+
continue
390+
case []any:
391+
// Fast path for []any - single pass without recursive calls for flat arrays
392+
for _, elem := range arr {
393+
switch e := elem.(type) {
394+
case int:
395+
total += float64(e)
396+
count++
397+
case float64:
398+
total += e
399+
count++
400+
case []int, []float64, []any:
401+
// Nested array - recurse
402+
nestedCount, nestedSum, err := mean(depth+1, e)
403+
if err != nil {
404+
return 0, 0, err
405+
}
406+
total += nestedSum
407+
count += nestedCount
408+
default:
409+
// Other numeric types or slices - use reflection
410+
rv := reflect.ValueOf(e)
411+
switch rv.Kind() {
412+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
413+
total += float64(rv.Int())
414+
count++
415+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
416+
total += float64(rv.Uint())
417+
count++
418+
case reflect.Float32, reflect.Float64:
419+
total += rv.Float()
420+
count++
421+
case reflect.Slice, reflect.Array:
422+
nestedCount, nestedSum, err := mean(depth+1, e)
423+
if err != nil {
424+
return 0, 0, err
425+
}
426+
total += nestedSum
427+
count += nestedCount
428+
default:
429+
return 0, 0, fmt.Errorf("invalid argument for mean (type %T)", e)
430+
}
431+
}
432+
}
433+
continue
434+
}
435+
436+
// Slow path: use reflection for other types
308437
rv := reflect.ValueOf(arg)
309438
switch rv.Kind() {
310439
case reflect.Array, reflect.Slice:
@@ -340,6 +469,56 @@ func median(depth int, args ...any) ([]float64, error) {
340469
var values []float64
341470

342471
for _, arg := range args {
472+
// Fast paths for common typed slices - avoid reflection and allocations
473+
switch arr := arg.(type) {
474+
case []int:
475+
for _, v := range arr {
476+
values = append(values, float64(v))
477+
}
478+
continue
479+
case []float64:
480+
values = append(values, arr...)
481+
continue
482+
case []any:
483+
// Fast path for []any - single pass without recursive calls for flat arrays
484+
for _, elem := range arr {
485+
switch e := elem.(type) {
486+
case int:
487+
values = append(values, float64(e))
488+
case float64:
489+
values = append(values, e)
490+
case []int, []float64, []any:
491+
// Nested array - recurse
492+
elems, err := median(depth+1, e)
493+
if err != nil {
494+
return nil, err
495+
}
496+
values = append(values, elems...)
497+
default:
498+
// Other numeric types or slices - use reflection
499+
rv := reflect.ValueOf(e)
500+
switch rv.Kind() {
501+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
502+
values = append(values, float64(rv.Int()))
503+
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
504+
values = append(values, float64(rv.Uint()))
505+
case reflect.Float32, reflect.Float64:
506+
values = append(values, rv.Float())
507+
case reflect.Slice, reflect.Array:
508+
elems, err := median(depth+1, e)
509+
if err != nil {
510+
return nil, err
511+
}
512+
values = append(values, elems...)
513+
default:
514+
return nil, fmt.Errorf("invalid argument for median (type %T)", e)
515+
}
516+
}
517+
}
518+
continue
519+
}
520+
521+
// Slow path: use reflection for other types
343522
rv := reflect.ValueOf(arg)
344523
switch rv.Kind() {
345524
case reflect.Array, reflect.Slice:

0 commit comments

Comments
 (0)