Skip to content

Instantly share code, notes, and snippets.

@rhnvrm
Last active June 11, 2024 09:08
Show Gist options
  • Save rhnvrm/db4567fcd87b2cb8e997999e1366d406 to your computer and use it in GitHub Desktop.
Save rhnvrm/db4567fcd87b2cb8e997999e1366d406 to your computer and use it in GitHub Desktop.
Benchmark Results of govaluate vs cel-go
package main
import (
"fmt"
"testing"
"github.com/Knetic/govaluate"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/interpreter/functions"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
func BenchmarkCelGo(b *testing.B) {
expr := "((TestDouble >= 1.0 || TestString.TestFunction() == 'HelloWorld') && (TestDouble + 1.0 >= 0.0)) || Now > TestTime"
decls := cel.Declarations(
// Identifiers used within this expression.
decls.NewIdent("TestDouble", decls.Double, nil),
decls.NewIdent("TestString", decls.String, nil),
decls.NewIdent("TestTime", decls.Uint, nil),
decls.NewIdent("Now", decls.Uint, nil),
decls.NewFunction("TestFunction", decls.NewInstanceOverload("TestFunction",
[]*exprpb.Type{decls.String},
decls.String)),
)
e, err := cel.NewEnv(decls)
if err != nil {
b.Fatalf("environment creation error: %s\n", err)
}
// Parse and check the expression.
p, iss := e.Parse(expr)
if iss != nil && iss.Err() != nil {
b.Fatal(iss.Err())
}
c, iss := e.Check(p)
if iss != nil && iss.Err() != nil {
b.Fatal(iss.Err())
}
// Create the program.
funcs := cel.Functions(
&functions.Overload{
Operator: "TestFunction",
Unary: func(value ref.Val) ref.Val {
ts, ok := value.(types.String)
if !ok {
return types.NewErr(
"invalid key of type '%v' and value '%v' to obj.get(key, def)",
value.Type(), ts)
}
return types.String("Hello" + ts)
},
})
prg, err := e.Program(c, funcs)
if err != nil {
b.Fatalf("program creation error: %s\n", err)
}
parameters := map[string]interface{}{
"TestDouble": 0.0,
"TestString": "World",
"TestTime": 0,
"Now": 1,
}
for n := 0; n < b.N; n++ {
prg.Eval(parameters)
}
}
func BenchmarkGovaluate(b *testing.B) {
expr := "((TestDouble >= 1.0 || TestFunction(TestString) == 'HelloWorld') && (TestDouble + 1.0 >= 0.0)) || Now > TestTime"
functions := map[string]govaluate.ExpressionFunction{
"TestFunction": func(args ...interface{}) (interface{}, error) {
ts, ok := args[0].(string)
if !ok {
return nil, fmt.Errorf("error: %v", ts)
}
return "Hello" + ts, nil
},
}
expression, _ := govaluate.NewEvaluableExpressionWithFunctions(expr, functions)
parameters := map[string]interface{}{
"TestDouble": 0.0,
"TestString": "World",
"TestTime": 0,
"Now": 1,
}
for n := 0; n < b.N; n++ {
expression.Evaluate(parameters)
}
}
go test -cpuprofile cpu.prof -memprofile mem.prof -benchmem -bench .
goos: linux
goarch: amd64
pkg: benchcelgo
BenchmarkCelGo-4 2000000 656 ns/op 88 B/op 5 allocs/op
BenchmarkGovaluate-4 2000000 702 ns/op 72 B/op 5 allocs/op
PASS
ok benchcelgo 4.321s
@RegisMelgaco
Copy link

RegisMelgaco commented Jul 24, 2023

What do you think about running more tests with different payload sizes so we can get an idea of how it scales? (Sorry for the pt-Br haha)

@rhnvrm
Copy link
Author

rhnvrm commented Jul 24, 2023

@RegisMelgaco We ended up using go-plugins instead for our usecase. You can read more here:

https://zerodha.tech/blog/a-lesson-in-niche-business-dsls-at-scale/

Although, I think there is also a place for expression engines like Govaluate and Cel-Go but it's best to evaluate on a case-by-case basis based on the requirements of your project.

@RegisMelgaco
Copy link

Thx!

@antonmedv
Copy link

antonmedv commented Jun 11, 2024

Another benchmark for different lang including cel-go and https://expr-lang.org:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment