Skip to content

Commit 2571efd

Browse files
committed
Improve the performance of runtime.Fetch for structures
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)
1 parent 1c09e5e commit 2571efd

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed

bench_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,30 @@ func Benchmark_envStruct(b *testing.B) {
266266
require.True(b, out.(bool))
267267
}
268268

269+
func Benchmark_envStruct_noEnv(b *testing.B) {
270+
type Price struct {
271+
Value int
272+
}
273+
type Env struct {
274+
Price Price
275+
}
276+
277+
program, err := expr.Compile(`Price.Value > 0`)
278+
require.NoError(b, err)
279+
280+
env := Env{Price: Price{Value: 1}}
281+
282+
var out any
283+
b.ResetTimer()
284+
for n := 0; n < b.N; n++ {
285+
out, err = vm.Run(program, env)
286+
}
287+
b.StopTimer()
288+
289+
require.NoError(b, err)
290+
require.True(b, out.(bool))
291+
}
292+
269293
func Benchmark_envMap(b *testing.B) {
270294
type Price struct {
271295
Value int

vm/runtime/runtime.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,18 @@ import (
66
"fmt"
77
"math"
88
"reflect"
9+
"sync"
910

1011
"github.com/expr-lang/expr/internal/deref"
1112
)
1213

14+
var fieldCache sync.Map
15+
16+
type fieldCacheKey struct {
17+
t reflect.Type
18+
f string
19+
}
20+
1321
func Fetch(from, i any) any {
1422
v := reflect.ValueOf(from)
1523
if v.Kind() == reflect.Invalid {
@@ -63,8 +71,17 @@ func Fetch(from, i any) any {
6371

6472
case reflect.Struct:
6573
fieldName := i.(string)
66-
value := v.FieldByNameFunc(func(name string) bool {
67-
field, _ := v.Type().FieldByName(name)
74+
t := v.Type()
75+
key := fieldCacheKey{
76+
t: t,
77+
f: fieldName,
78+
}
79+
if fi, ok := fieldCache.Load(key); ok {
80+
field := fi.(*reflect.StructField)
81+
return v.FieldByIndex(field.Index).Interface()
82+
}
83+
field, ok := t.FieldByNameFunc(func(name string) bool {
84+
field, _ := t.FieldByName(name)
6885
switch field.Tag.Get("expr") {
6986
case "-":
7087
return false
@@ -74,8 +91,12 @@ func Fetch(from, i any) any {
7491
return name == fieldName
7592
}
7693
})
77-
if value.IsValid() {
78-
return value.Interface()
94+
if ok {
95+
value := v.FieldByIndex(field.Index)
96+
if value.IsValid() {
97+
fieldCache.Store(key, &field)
98+
return value.Interface()
99+
}
79100
}
80101
}
81102
panic(fmt.Sprintf("cannot fetch %v from %T", i, from))

0 commit comments

Comments
 (0)