Skip to content

Commit b9bb62d

Browse files
committed
perf(builtin): add fast paths for min, max, mean, and median
Avoid reflection and per-element allocations for common typed slices ([]int, []float64, []any) by adding type-switch fast paths that iterate directly without calling reflect.Value.Interface(). Falls back to reflection for other slice types to maintain compatibility. Signed-off-by: Ville Vesilehto <ville@vesilehto.fi>
1 parent 1fa0282 commit b9bb62d

File tree

2 files changed

+365
-1
lines changed

2 files changed

+365
-1
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/lib.go

Lines changed: 297 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 - single pass without recursive calls
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,135 @@ 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 int8:
398+
total += float64(e)
399+
count++
400+
case int16:
401+
total += float64(e)
402+
count++
403+
case int32:
404+
total += float64(e)
405+
count++
406+
case int64:
407+
total += float64(e)
408+
count++
409+
case uint:
410+
total += float64(e)
411+
count++
412+
case uint8:
413+
total += float64(e)
414+
count++
415+
case uint16:
416+
total += float64(e)
417+
count++
418+
case uint32:
419+
total += float64(e)
420+
count++
421+
case uint64:
422+
total += float64(e)
423+
count++
424+
case float32:
425+
total += float64(e)
426+
count++
427+
case float64:
428+
total += e
429+
count++
430+
case []int, []float64, []any:
431+
// Nested array - recurse
432+
nestedCount, nestedSum, err := mean(depth+1, e)
433+
if err != nil {
434+
return 0, 0, err
435+
}
436+
total += nestedSum
437+
count += nestedCount
438+
default:
439+
// Could be another slice type, use reflection
440+
rv := reflect.ValueOf(e)
441+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
442+
nestedCount, nestedSum, err := mean(depth+1, e)
443+
if err != nil {
444+
return 0, 0, err
445+
}
446+
total += nestedSum
447+
count += nestedCount
448+
} else {
449+
return 0, 0, fmt.Errorf("invalid argument for mean (type %T)", e)
450+
}
451+
}
452+
}
453+
continue
454+
case int:
455+
total += float64(arr)
456+
count++
457+
continue
458+
case int8:
459+
total += float64(arr)
460+
count++
461+
continue
462+
case int16:
463+
total += float64(arr)
464+
count++
465+
continue
466+
case int32:
467+
total += float64(arr)
468+
count++
469+
continue
470+
case int64:
471+
total += float64(arr)
472+
count++
473+
continue
474+
case uint:
475+
total += float64(arr)
476+
count++
477+
continue
478+
case uint8:
479+
total += float64(arr)
480+
count++
481+
continue
482+
case uint16:
483+
total += float64(arr)
484+
count++
485+
continue
486+
case uint32:
487+
total += float64(arr)
488+
count++
489+
continue
490+
case uint64:
491+
total += float64(arr)
492+
count++
493+
continue
494+
case float32:
495+
total += float64(arr)
496+
count++
497+
continue
498+
case float64:
499+
total += arr
500+
count++
501+
continue
502+
}
503+
504+
// Slow path: use reflection for other types
308505
rv := reflect.ValueOf(arg)
309506
switch rv.Kind() {
310507
case reflect.Array, reflect.Slice:
@@ -340,6 +537,105 @@ func median(depth int, args ...any) ([]float64, error) {
340537
var values []float64
341538

342539
for _, arg := range args {
540+
// Fast paths for common typed slices - avoid reflection and allocations
541+
switch arr := arg.(type) {
542+
case []int:
543+
for _, v := range arr {
544+
values = append(values, float64(v))
545+
}
546+
continue
547+
case []float64:
548+
values = append(values, arr...)
549+
continue
550+
case []any:
551+
// Fast path for []any - single pass without recursive calls for flat arrays
552+
for _, elem := range arr {
553+
switch e := elem.(type) {
554+
case int:
555+
values = append(values, float64(e))
556+
case int8:
557+
values = append(values, float64(e))
558+
case int16:
559+
values = append(values, float64(e))
560+
case int32:
561+
values = append(values, float64(e))
562+
case int64:
563+
values = append(values, float64(e))
564+
case uint:
565+
values = append(values, float64(e))
566+
case uint8:
567+
values = append(values, float64(e))
568+
case uint16:
569+
values = append(values, float64(e))
570+
case uint32:
571+
values = append(values, float64(e))
572+
case uint64:
573+
values = append(values, float64(e))
574+
case float32:
575+
values = append(values, float64(e))
576+
case float64:
577+
values = append(values, e)
578+
case []int, []float64, []any:
579+
// Nested array - recurse
580+
elems, err := median(depth+1, e)
581+
if err != nil {
582+
return nil, err
583+
}
584+
values = append(values, elems...)
585+
default:
586+
// Could be another slice type, use reflection
587+
rv := reflect.ValueOf(e)
588+
if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array {
589+
elems, err := median(depth+1, e)
590+
if err != nil {
591+
return nil, err
592+
}
593+
values = append(values, elems...)
594+
} else {
595+
return nil, fmt.Errorf("invalid argument for median (type %T)", e)
596+
}
597+
}
598+
}
599+
continue
600+
case int:
601+
values = append(values, float64(arr))
602+
continue
603+
case int8:
604+
values = append(values, float64(arr))
605+
continue
606+
case int16:
607+
values = append(values, float64(arr))
608+
continue
609+
case int32:
610+
values = append(values, float64(arr))
611+
continue
612+
case int64:
613+
values = append(values, float64(arr))
614+
continue
615+
case uint:
616+
values = append(values, float64(arr))
617+
continue
618+
case uint8:
619+
values = append(values, float64(arr))
620+
continue
621+
case uint16:
622+
values = append(values, float64(arr))
623+
continue
624+
case uint32:
625+
values = append(values, float64(arr))
626+
continue
627+
case uint64:
628+
values = append(values, float64(arr))
629+
continue
630+
case float32:
631+
values = append(values, float64(arr))
632+
continue
633+
case float64:
634+
values = append(values, arr)
635+
continue
636+
}
637+
638+
// Slow path: use reflection for other types
343639
rv := reflect.ValueOf(arg)
344640
switch rv.Kind() {
345641
case reflect.Array, reflect.Slice:

0 commit comments

Comments
 (0)