From 01b16d4794bc77aed766b2845c4f032e5866c86a Mon Sep 17 00:00:00 2001 From: montag451 Date: Mon, 15 Sep 2025 23:10:04 +0200 Subject: [PATCH] Improve the performance of runtime.Fetch for structures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The improvement comes from the use of a cache to speed up the retrieval of struct fields. This commit also adds a new benchmark to measure the performance gain. goos: linux goarch: amd64 pkg: github.com/expr-lang/expr cpu: Intel(R) Core(TM) i7-8650U CPU @ 1.90GHz │ /tmp/old.txt │ /tmp/new.txt │ │ sec/op │ sec/op vs base │ _envStruct_noEnv-8 503.3n ± 4% 228.8n ± 3% -54.53% (p=0.000 n=10) │ /tmp/old.txt │ /tmp/new.txt │ │ B/op │ B/op vs base │ _envStruct_noEnv-8 48.00 ± 0% 32.00 ± 0% -33.33% (p=0.000 n=10) │ /tmp/old.txt │ /tmp/new.txt │ │ allocs/op │ allocs/op vs base │ _envStruct_noEnv-8 3.000 ± 0% 1.000 ± 0% -66.67% (p=0.000 n=10) --- bench_test.go | 24 ++++++++++++++++++++++++ vm/runtime/runtime.go | 29 +++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/bench_test.go b/bench_test.go index c9f2b718..d70a0935 100644 --- a/bench_test.go +++ b/bench_test.go @@ -266,6 +266,30 @@ func Benchmark_envStruct(b *testing.B) { require.True(b, out.(bool)) } +func Benchmark_envStruct_noEnv(b *testing.B) { + type Price struct { + Value int + } + type Env struct { + Price Price + } + + program, err := expr.Compile(`Price.Value > 0`) + require.NoError(b, err) + + env := Env{Price: Price{Value: 1}} + + var out any + b.ResetTimer() + for n := 0; n < b.N; n++ { + out, err = vm.Run(program, env) + } + b.StopTimer() + + require.NoError(b, err) + require.True(b, out.(bool)) +} + func Benchmark_envMap(b *testing.B) { type Price struct { Value int diff --git a/vm/runtime/runtime.go b/vm/runtime/runtime.go index 56759c53..4780c71e 100644 --- a/vm/runtime/runtime.go +++ b/vm/runtime/runtime.go @@ -6,10 +6,18 @@ import ( "fmt" "math" "reflect" + "sync" "github.com/expr-lang/expr/internal/deref" ) +var fieldCache sync.Map + +type fieldCacheKey struct { + t reflect.Type + f string +} + func Fetch(from, i any) any { v := reflect.ValueOf(from) if v.Kind() == reflect.Invalid { @@ -63,8 +71,17 @@ func Fetch(from, i any) any { case reflect.Struct: fieldName := i.(string) - value := v.FieldByNameFunc(func(name string) bool { - field, _ := v.Type().FieldByName(name) + t := v.Type() + key := fieldCacheKey{ + t: t, + f: fieldName, + } + if fi, ok := fieldCache.Load(key); ok { + field := fi.(*reflect.StructField) + return v.FieldByIndex(field.Index).Interface() + } + field, ok := t.FieldByNameFunc(func(name string) bool { + field, _ := t.FieldByName(name) switch field.Tag.Get("expr") { case "-": return false @@ -74,8 +91,12 @@ func Fetch(from, i any) any { return name == fieldName } }) - if value.IsValid() { - return value.Interface() + if ok { + value := v.FieldByIndex(field.Index) + if value.IsValid() { + fieldCache.Store(key, &field) + return value.Interface() + } } } panic(fmt.Sprintf("cannot fetch %v from %T", i, from))