diff --git a/bench_test.go b/bench_test.go index d70a0935..99fd4aab 100644 --- a/bench_test.go +++ b/bench_test.go @@ -571,3 +571,71 @@ func Benchmark_reduce(b *testing.B) { require.Equal(b, 5050, out.(int)) } + +func Benchmark_min(b *testing.B) { + 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} + env := map[string]any{"arr": arr} + + program, err := expr.Compile(`min(arr)`, expr.Env(env)) + require.NoError(b, err) + + var out any + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, _ = vm.Run(program, env) + } + b.StopTimer() + + require.Equal(b, 1, out) +} + +func Benchmark_max(b *testing.B) { + 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} + env := map[string]any{"arr": arr} + + program, err := expr.Compile(`max(arr)`, expr.Env(env)) + require.NoError(b, err) + + var out any + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, _ = vm.Run(program, env) + } + b.StopTimer() + + require.Equal(b, 100, out) +} + +func Benchmark_mean(b *testing.B) { + 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} + env := map[string]any{"arr": arr} + + program, err := expr.Compile(`mean(arr)`, expr.Env(env)) + require.NoError(b, err) + + var out any + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, _ = vm.Run(program, env) + } + b.StopTimer() + + require.Equal(b, 50.5, out) +} + +func Benchmark_median(b *testing.B) { + 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} + env := map[string]any{"arr": arr} + + program, err := expr.Compile(`median(arr)`, expr.Env(env)) + require.NoError(b, err) + + var out any + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, _ = vm.Run(program, env) + } + b.StopTimer() + + require.Equal(b, 50.5, out) +} diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index a5dabbbb..c25b98ae 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -21,11 +21,19 @@ import ( func TestBuiltin(t *testing.T) { ArrayWithNil := []any{42} env := map[string]any{ - "ArrayOfString": []string{"foo", "bar", "baz"}, - "ArrayOfInt": []int{1, 2, 3}, - "ArrayOfAny": []any{1, "2", true}, - "ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}}, - "PtrArrayWithNil": &ArrayWithNil, + "ArrayOfString": []string{"foo", "bar", "baz"}, + "ArrayOfInt": []int{1, 2, 3}, + "ArrayOfFloat": []float64{1.5, 2.5, 3.5}, + "ArrayOfInt32": []int32{1, 2, 3}, + "ArrayOfAny": []any{1, "2", true}, + "ArrayOfFoo": []mock.Foo{{Value: "a"}, {Value: "b"}, {Value: "c"}}, + "PtrArrayWithNil": &ArrayWithNil, + "EmptyIntArray": []int{}, + "EmptyFloatArray": []float64{}, + "NestedIntArrays": []any{[]int{1, 2}, []int{3, 4}}, + "NestedAnyArrays": []any{[]any{1, 2}, []any{3, 4}}, + "MixedNestedArray": []any{1, []int{2, 3}, []float64{4.0, 5.0}}, + "NestedInt32Array": []any{[]int32{1, 2}, []int32{3, 4}}, } var tests = []struct { @@ -86,6 +94,22 @@ func TestBuiltin(t *testing.T) { {`min([1, 2, 3])`, 1}, {`min([1.5, 2.5, 3.5])`, 1.5}, {`min(-1, [1.5, 2.5, 3.5])`, -1}, + {`max(ArrayOfInt)`, 3}, + {`min(ArrayOfInt)`, 1}, + {`max(ArrayOfFloat)`, 3.5}, + {`min(ArrayOfFloat)`, 1.5}, + {`max(EmptyIntArray, 5)`, 5}, + {`min(EmptyFloatArray, 5)`, 5}, + {`max(NestedIntArrays)`, 4}, + {`min(NestedIntArrays)`, 1}, + {`max(NestedAnyArrays)`, 4}, + {`min(NestedAnyArrays)`, 1}, + {`max(MixedNestedArray)`, 5.0}, + {`min(MixedNestedArray)`, 1}, + {`max(ArrayOfInt32)`, int32(3)}, + {`min(ArrayOfInt32)`, int32(1)}, + {`max(NestedInt32Array)`, int32(4)}, + {`min(NestedInt32Array)`, int32(1)}, {`sum(1..9)`, 45}, {`sum([.5, 1.5, 2.5])`, 4.5}, {`sum([])`, 0}, @@ -97,6 +121,13 @@ func TestBuiltin(t *testing.T) { {`mean(10, [1, 2, 3], 1..9)`, 4.6923076923076925}, {`mean(-10, [1, 2, 3, 4])`, 0.0}, {`mean(10.9, 1..9)`, 5.59}, + {`mean(ArrayOfInt)`, 2.0}, + {`mean(ArrayOfFloat)`, 2.5}, + {`mean(NestedIntArrays)`, 2.5}, + {`mean(NestedAnyArrays)`, 2.5}, + {`mean(MixedNestedArray)`, 3.0}, + {`mean(ArrayOfInt32)`, 2.0}, + {`mean(NestedInt32Array)`, 2.5}, {`median(1..9)`, 5.0}, {`median([.5, 1.5, 2.5])`, 1.5}, {`median([])`, 0.0}, @@ -105,6 +136,13 @@ func TestBuiltin(t *testing.T) { {`median(10, [1, 2, 3], 1..9)`, 4.0}, {`median(-10, [1, 2, 3, 4])`, 2.0}, {`median(1..5, 4.9)`, 3.5}, + {`median(ArrayOfInt)`, 2.0}, + {`median(ArrayOfFloat)`, 2.5}, + {`median(NestedIntArrays)`, 2.5}, + {`median(NestedAnyArrays)`, 2.5}, + {`median(MixedNestedArray)`, 3.0}, + {`median(ArrayOfInt32)`, 2.0}, + {`median(NestedInt32Array)`, 2.5}, {`toJSON({foo: 1, bar: 2})`, "{\n \"bar\": 2,\n \"foo\": 1\n}"}, {`fromJSON("[1, 2, 3]")`, []any{1.0, 2.0, 3.0}}, {`toBase64("hello")`, "aGVsbG8="}, diff --git a/builtin/lib.go b/builtin/lib.go index 07a029b2..40ba19e9 100644 --- a/builtin/lib.go +++ b/builtin/lib.go @@ -259,6 +259,75 @@ func minMax(name string, fn func(any, any) bool, depth int, args ...any) (any, e } var val any for _, arg := range args { + // Fast paths for common typed slices - avoid reflection and allocations + switch arr := arg.(type) { + case []int: + if len(arr) == 0 { + continue + } + m := arr[0] + for i := 1; i < len(arr); i++ { + if fn(m, arr[i]) { + m = arr[i] + } + } + if val == nil || fn(val, m) { + val = m + } + continue + case []float64: + if len(arr) == 0 { + continue + } + m := arr[0] + for i := 1; i < len(arr); i++ { + if fn(m, arr[i]) { + m = arr[i] + } + } + if val == nil || fn(val, m) { + val = m + } + continue + case []any: + // Fast path for []any with simple numeric types + for _, elem := range arr { + switch e := elem.(type) { + case int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64, + float32, float64: + if val == nil || fn(val, e) { + val = e + } + case []int, []float64, []any: + // Nested array - recurse + nested, err := minMax(name, fn, depth+1, e) + if err != nil { + return nil, err + } + if nested != nil && (val == nil || fn(val, nested)) { + val = nested + } + default: + // Could be another slice type, use reflection + rv := reflect.ValueOf(e) + if rv.Kind() == reflect.Slice || rv.Kind() == reflect.Array { + nested, err := minMax(name, fn, depth+1, e) + if err != nil { + return nil, err + } + if nested != nil && (val == nil || fn(val, nested)) { + val = nested + } + } else { + return nil, fmt.Errorf("invalid argument for %s (type %T)", name, e) + } + } + } + continue + } + + // Slow path: use reflection for other types rv := reflect.ValueOf(arg) switch rv.Kind() { case reflect.Array, reflect.Slice: @@ -278,7 +347,6 @@ func minMax(name string, fn func(any, any) bool, depth int, args ...any) (any, e default: return nil, fmt.Errorf("invalid argument for %s (type %T)", name, elemVal) } - } case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, @@ -305,6 +373,67 @@ func mean(depth int, args ...any) (int, float64, error) { var count int for _, arg := range args { + // Fast paths for common typed slices - avoid reflection and allocations + switch arr := arg.(type) { + case []int: + for _, v := range arr { + total += float64(v) + } + count += len(arr) + continue + case []float64: + for _, v := range arr { + total += v + } + count += len(arr) + continue + case []any: + // Fast path for []any - single pass without recursive calls for flat arrays + for _, elem := range arr { + switch e := elem.(type) { + case int: + total += float64(e) + count++ + case float64: + total += e + count++ + case []int, []float64, []any: + // Nested array - recurse + nestedCount, nestedSum, err := mean(depth+1, e) + if err != nil { + return 0, 0, err + } + total += nestedSum + count += nestedCount + default: + // Other numeric types or slices - use reflection + rv := reflect.ValueOf(e) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + total += float64(rv.Int()) + count++ + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + total += float64(rv.Uint()) + count++ + case reflect.Float32, reflect.Float64: + total += rv.Float() + count++ + case reflect.Slice, reflect.Array: + nestedCount, nestedSum, err := mean(depth+1, e) + if err != nil { + return 0, 0, err + } + total += nestedSum + count += nestedCount + default: + return 0, 0, fmt.Errorf("invalid argument for mean (type %T)", e) + } + } + } + continue + } + + // Slow path: use reflection for other types rv := reflect.ValueOf(arg) switch rv.Kind() { case reflect.Array, reflect.Slice: @@ -340,6 +469,56 @@ func median(depth int, args ...any) ([]float64, error) { var values []float64 for _, arg := range args { + // Fast paths for common typed slices - avoid reflection and allocations + switch arr := arg.(type) { + case []int: + for _, v := range arr { + values = append(values, float64(v)) + } + continue + case []float64: + values = append(values, arr...) + continue + case []any: + // Fast path for []any - single pass without recursive calls for flat arrays + for _, elem := range arr { + switch e := elem.(type) { + case int: + values = append(values, float64(e)) + case float64: + values = append(values, e) + case []int, []float64, []any: + // Nested array - recurse + elems, err := median(depth+1, e) + if err != nil { + return nil, err + } + values = append(values, elems...) + default: + // Other numeric types or slices - use reflection + rv := reflect.ValueOf(e) + switch rv.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + values = append(values, float64(rv.Int())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + values = append(values, float64(rv.Uint())) + case reflect.Float32, reflect.Float64: + values = append(values, rv.Float()) + case reflect.Slice, reflect.Array: + elems, err := median(depth+1, e) + if err != nil { + return nil, err + } + values = append(values, elems...) + default: + return nil, fmt.Errorf("invalid argument for median (type %T)", e) + } + } + } + continue + } + + // Slow path: use reflection for other types rv := reflect.ValueOf(arg) switch rv.Kind() { case reflect.Array, reflect.Slice: