Files
expr/checker/checker_bench_test.go
T
a60d994e25 Improve Performance of the Type System (#824)
* export the checker and ensure no methods or fields are exported

* make Checker reusable

* add PatchAndCheck method to Checker, and reuse it in visitors

* avoid unnecessary map and consolidate structs

* make all Nature methods use pointer-receiver

* added cache for type and nature information

* optimize type and nature checks

* more aggressively cache some partial results and add cheap checks

* cache reused values

* reduce copying in Nature by breaking down optional fields

* enforce use of cache

* improve ergonomics of FromType and NatureOf

* improve struct method fetching

* make cache private to Nature

* first iteration improving struct reflect caching

* cleaned up struct fields data

* improve caching of functions data

* simplify checker code

* simplify and remove dead code

* add checker benchmarks

* address PR comments and minor improvements

* rename from SetCache to Bind

* make the benchmarks more fair

* fix benchmark option not being applied

* move Cache to be argument instead of field

* merge FuncData into Optional

* rename Optional to TypeData

* simplify struct logic

* cache builtin numeric types

---------

Co-authored-by: Anton Medvedev <[email protected]>
2025-09-18 14:38:27 +02:00

115 lines
3.2 KiB
Go

package checker_test
import (
"runtime"
"testing"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/checker"
"github.com/expr-lang/expr/checker/nature"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/parser"
)
func BenchmarkChecker(b *testing.B) {
cases := []struct {
name, input string
}{
{"function calls", `
func(
func(
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
),
func(
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
),
func(
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
func(func(a, 'a', 1, nil), func(a, 'a', 1, nil), func(a, 'a', 1, nil)),
)
)
`},
{"unary and binary operations", `
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1 &&
!b && !b || !b == !b && !b != !b || 1 < 1.0 && 0.1 > 1 || 0 <= 1.0 && 0.1 >= 1
`},
{"deep struct access", `
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.
a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a
`},
}
f := func(params ...any) (any, error) { return nil, nil }
env := map[string]any{
"a": new(recursive),
"b": true,
"func": f,
}
config := conf.New(env)
expr.Function("func", f, f)(config)
expr.ConstExpr("func")(config)
for _, c := range cases {
batchSize := 100_000
if batchSize > b.N {
batchSize = b.N
}
trees := make([]*parser.Tree, 0, batchSize)
for i := 0; i < batchSize; i++ {
tree, err := parser.ParseWithConfig(c.input, config)
if err != nil {
b.Fatal(err)
}
trees = append(trees, tree)
}
runtime.GC() // try to cleanup the mess from the initialization
b.Run("name="+c.name, func(b *testing.B) {
var err error
for i := 0; i < b.N; i++ {
j := i
if j < 0 || j >= len(trees) {
b.StopTimer()
invalidateTrees(trees...)
j = 0
b.StartTimer()
}
_, err = checker.Check(trees[j], config)
}
b.StopTimer()
if err != nil {
b.Fatal(err)
}
})
}
}
type visitorFunc func(*ast.Node)
func (f visitorFunc) Visit(node *ast.Node) { f(node) }
func invalidateTrees(trees ...*parser.Tree) {
for _, tree := range trees {
ast.Walk(&tree.Node, visitorFunc(func(node *ast.Node) {
(*node).SetNature(nature.Nature{})
}))
}
}
type recursive struct {
Inner *recursive `expr:"a"`
}