From f0a7d7664653b131db57debdf56948c3fa4db02c Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 2 Feb 2024 11:44:42 +0100 Subject: [PATCH 1/2] feat(gnovm): add benchmark system --- gnovm/pkg/gnolang/benchdata/fib.gno | 16 + gnovm/pkg/gnolang/benchdata/loop.gno | 5 + gnovm/pkg/gnolang/benchdata/matrix_mult.gno | 90 +++++ gnovm/pkg/gnolang/gno_test.go | 103 +++-- gnovm/pkg/gnolang/go_bench_test.go | 420 -------------------- 5 files changed, 186 insertions(+), 448 deletions(-) create mode 100644 gnovm/pkg/gnolang/benchdata/fib.gno create mode 100644 gnovm/pkg/gnolang/benchdata/loop.gno create mode 100644 gnovm/pkg/gnolang/benchdata/matrix_mult.gno delete mode 100644 gnovm/pkg/gnolang/go_bench_test.go diff --git a/gnovm/pkg/gnolang/benchdata/fib.gno b/gnovm/pkg/gnolang/benchdata/fib.gno new file mode 100644 index 00000000000..75f2b0b15d6 --- /dev/null +++ b/gnovm/pkg/gnolang/benchdata/fib.gno @@ -0,0 +1,16 @@ +// param: 4 8 16 + +package main + +func main() { + for i := 0; i < {{ .N }}; i++ { + fib({{ .Param }}) + } +} + +func fib(n int) int { + if n < 2 { + return 1 + } + return fib(n-1) + fib(n-2) +} diff --git a/gnovm/pkg/gnolang/benchdata/loop.gno b/gnovm/pkg/gnolang/benchdata/loop.gno new file mode 100644 index 00000000000..0effa823f0d --- /dev/null +++ b/gnovm/pkg/gnolang/benchdata/loop.gno @@ -0,0 +1,5 @@ +package main + +func main() { + for i := 0; i < {{ .N }}; i++ {} +} diff --git a/gnovm/pkg/gnolang/benchdata/matrix_mult.gno b/gnovm/pkg/gnolang/benchdata/matrix_mult.gno new file mode 100644 index 00000000000..429323df65d --- /dev/null +++ b/gnovm/pkg/gnolang/benchdata/matrix_mult.gno @@ -0,0 +1,90 @@ +// param: 3 4 5 6 + +package main + +func main() { + const p = {{ .Param }} + for i := 0; i < {{ .N }}; i++ { + _ = det(mul(dist(p), mul(id(p), dist(p)))) + } +} + +// identity matrix +func id(sz int) [][]int { + r := make([][]int, sz) + for i := range r { + r[i] = make([]int, sz) + r[i][i] = 1 + } + return r +} + +// distance from corner +func dist(sz int) [][]int { + half := sz / 2 + r := make([][]int, sz) + for i := range r { + r[i] = make([]int, sz) + vdist := i + if vdist >= half { + vdist = sz - 1 - i + } + for j := range r[i] { + hdist := j + if hdist >= half { + hdist = sz - 1 - j + } + r[i][j] = vdist + hdist + } + } + return r +} + +func det(m [][]int) int { + size := len(m) + if size == 2 { + return m[0][0]*m[1][1] - m[0][1]*m[1][0] + } + subMatrix := make([][]int, size-1) + for j := range subMatrix { + subMatrix[j] = make([]int, size-1) + } + + determinant := 0 + for i := 0; i < size; i++ { + for j := 1; j < size; j++ { + t := 0 + for k := 0; k < size; k++ { + if k == i { + continue + } + subMatrix[j-1][t] = m[j][k] + t++ + } + } + sign := 1 + if i % 2 == 1 { + sign = -1 + } + determinant += m[0][i] * det(subMatrix) * sign + } + return determinant +} + +func mul(m1, m2 [][]int) [][]int { + size := len(m1) + result := make([][]int, size) + for i := range result { + result[i] = make([]int, size) + } + + for i := 0; i < size; i++ { + for j := 0; j < size; j++ { + for k := 0; k < size; k++ { + result[i][j] += m1[i][k] * m2[k][j] + } + } + } + + return result +} diff --git a/gnovm/pkg/gnolang/gno_test.go b/gnovm/pkg/gnolang/gno_test.go index 5c50b0830a6..d8bdcdfd4a0 100644 --- a/gnovm/pkg/gnolang/gno_test.go +++ b/gnovm/pkg/gnolang/gno_test.go @@ -3,12 +3,18 @@ package gnolang import ( "bytes" "fmt" + "io" + "os" + "path/filepath" "reflect" + "strings" "testing" + "text/template" "unsafe" // "github.com/davecgh/go-spew/spew" "github.com/jaekwon/testify/assert" + "github.com/jaekwon/testify/require" ) // run empty main(). @@ -192,43 +198,84 @@ func main() { // Benchmarks func BenchmarkPreprocess(b *testing.B) { - for i := 0; i < b.N; i++ { - // stop timer - b.StopTimer() - pkg := &PackageNode{ - PkgName: "main", - PkgPath: ".main", - FileSet: nil, - } - pkg.InitStaticBlock(pkg, nil) - main := FuncD("main", nil, nil, Ss( - A("mx", ":=", "1000000"), - For( - A("i", ":=", "0"), - X("i < mx"), - Inc("i"), - ), - )) - b.StartTimer() - // timer started - main = Preprocess(nil, pkg, main).(*FuncDecl) + pkg := &PackageNode{ + PkgName: "main", + PkgPath: ".main", + FileSet: nil, } -} - -func BenchmarkLoopyMain(b *testing.B) { - m := NewMachine("test", nil) + pkg.InitStaticBlock(pkg, nil) main := FuncD("main", nil, nil, Ss( - A("mx", ":=", "10000000"), + A("mx", ":=", "1000000"), For( A("i", ":=", "0"), - // X("i < 10000000"), X("i < mx"), Inc("i"), ), )) - m.RunDeclaration(main) + copies := make([]*FuncDecl, b.N) + for i := 0; i < b.N; i++ { + copies[i] = main.Copy().(*FuncDecl) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { - m.RunMain() + main = Preprocess(nil, pkg, copies[i]).(*FuncDecl) + } +} + +type bdataParams struct { + N int + Param string +} + +func BenchmarkBenchdata(b *testing.B) { + const bdDir = "./benchdata" + files, err := os.ReadDir(bdDir) + require.NoError(b, err) + for _, file := range files { + // Read file and parse template. + bcont, err := os.ReadFile(filepath.Join(bdDir, file.Name())) + cont := string(bcont) + require.NoError(b, err) + tpl, err := template.New("").Parse(cont) + require.NoError(b, err) + + // Determine parameters. + const paramString = "// param: " + var params []string + pos := strings.Index(cont, paramString) + if pos >= 0 { + paramsRaw := strings.SplitN(cont[pos+len(paramString):], "\n", 2)[0] + params = strings.Fields(paramsRaw) + } else { + params = []string{""} + } + + for _, param := range params { + name := file.Name() + if param != "" { + name += "_param:" + param + } + b.Run(name, func(b *testing.B) { + // Gen template with N and param. + var buf bytes.Buffer + require.NoError(b, tpl.Execute(&buf, bdataParams{ + N: b.N, + Param: param, + })) + + // Set up machine. + m := NewMachineWithOptions(MachineOptions{ + PkgPath: "main", + Output: io.Discard, + }) + n := MustParseFile("main.go", buf.String()) + m.RunFiles(n) + + b.ResetTimer() + m.RunMain() + }) + } } } diff --git a/gnovm/pkg/gnolang/go_bench_test.go b/gnovm/pkg/gnolang/go_bench_test.go deleted file mode 100644 index 837d7625f35..00000000000 --- a/gnovm/pkg/gnolang/go_bench_test.go +++ /dev/null @@ -1,420 +0,0 @@ -package gnolang - -import ( - "fmt" - "reflect" - "testing" -) - -type BenchValue interface { - Int32() int32 -} - -type ( - Int32 int32 - Int32a int32 - Int32b int32 - Int32c int32 - Int32d int32 - Int32e int32 - Int32f int32 - Int32g int32 - Int32h int32 - Int32i int32 - Int32j int32 - Int32k int32 -) - -func (i Int32) Int32() int32 { return int32(i) } -func (i Int32a) Int32() int32 { return int32(i) } -func (i Int32b) Int32() int32 { return int32(i) } -func (i Int32c) Int32() int32 { return int32(i) } -func (i Int32d) Int32() int32 { return int32(i) } -func (i Int32e) Int32() int32 { return int32(i) } -func (i Int32f) Int32() int32 { return int32(i) } -func (i Int32g) Int32() int32 { return int32(i) } -func (i Int32h) Int32() int32 { return int32(i) } -func (i Int32i) Int32() int32 { return int32(i) } -func (i Int32j) Int32() int32 { return int32(i) } -func (i Int32k) Int32() int32 { return int32(i) } - -func BenchmarkMapSet(b *testing.B) { - m := make(map[int32]int32) - for i := 0; i < b.N; i++ { - m[int32(i%20)] = int32(i) - } -} - -func BenchmarkMapCreateSet(b *testing.B) { - for i := 0; i < b.N; i++ { - m := make(map[int32]int32) - m[int32(i%20)] = int32(i) - } -} - -func BenchmarkMapCreateSetString(b *testing.B) { - for i := 0; i < b.N; i++ { - m := make(map[string]int32) - m["5"] += 1 - } -} - -// shows that it might be kinda worth it to not use maps but slices for struct -// fields, but for small structs. -func BenchmarkSliceIterate10(b *testing.B) { - fs := []TestField{} - for i := 0; i < 10; i++ { - fs = append(fs, TestField{fmt.Sprintf("%v", i%10), int32(i)}) - } - for i := 0; i < b.N; i++ { - i10 := i % 10 - for j := 0; j < 10; j++ { - if fs[i10].Name == "5" { - fs[i10].Value += 1 - } - } - } - b.Log(fs) -} - -type SomeStruct struct { - Field1 int32 - Field2 int32 -} - -func (s SomeStruct) it() int32 { - return s.Field1 + s.Field2 -} - -// seems to inline. -func BenchmarkStructStack(b *testing.B) { - x := int32(0) - for i := 0; i < b.N; i++ { - s := SomeStruct{Field1: int32(i) % 20, Field2: 1} - x = s.it() - } - b.Log(x) -} - -// this doesn't work -func BenchmarkStructGC(b *testing.B) { - x := int32(0) - gen := func(i int) *SomeStruct { return &SomeStruct{Field1: int32(i) % 20} } - for i := 0; i < b.N; i++ { - s := gen(i) - x = s.Field1 - } - b.Log(x) -} - -type TestField struct { - Name string - Value int32 -} - -func BenchmarkTypeAssertionMethodCall(b *testing.B) { - // This uses no interface. - b.Run("Int32().Int32() (no interface)", func(b *testing.B) { - var v Int32 = Int32(1) - x := int32(0) - for i := 0; i < b.N; i++ { - x += v.Int32() - } - }) - // This calls a method on the interface. - // It's surprising that this is slower than switch concrete assert method - // by an order of magnitude when the alternative enables inlining. - // Perhaps go could do better by first grouping each interface into a - // single giant switch statement. - b.Run("BenchValue().Int32() (interface method)", func(b *testing.B) { - var v BenchValue = Int32(1) - x := int32(0) - for i := 0; i < b.N; i++ { - x += v.Int32() - } - }) - // This type-asserts to a concrete type and calls its method. - b.Run("v.(Int32).Int32() (concrete assert method)", func(b *testing.B) { - var v interface{} = Int32(1) - x := int32(0) - for i := 0; i < b.N; i++ { - x += v.(Int32).Int32() - } - }) - // This switch-type-asserts to a concrete type and calls its method. - // This actually ends up being the best choice, and is even faster than - // calling a method on an interface. - b.Run("case v.(Int32).Int32() (type switch concrete assert method)", func(b *testing.B) { - var v interface{} = Int32(1) - x := int32(0) - for i := 0; i < b.N; i++ { - switch v := v.(type) { - case Int32: - x += v.Int32() - case Int32a: - x += v.Int32() + 1 - case Int32b: - x += v.Int32() + 2 - case Int32c: - x += v.Int32() + 3 - case Int32d: - x += v.Int32() + 4 - case Int32e: - x += v.Int32() + 5 - case Int32f: - x += v.Int32() + 6 - case Int32g: - x += v.Int32() + 7 - case Int32h: - x += v.Int32() + 8 - case Int32i: - x += v.Int32() + 9 - case Int32j: - x += v.Int32() + 10 - case Int32k: - x += v.Int32() + 11 - default: - panic("should not happen") - } - } - }) - // This appears to run fast, not sure what optimization is happening, - // but maybe the initial interface setting is fine as the itable - // info is known statically. - b.Run("MyStruct{Value:Int32(i)} (struct interface field init)", func(b *testing.B) { - type MyStruct struct { - Value BenchValue - } - x := int32(0) - for i := 0; i < b.N; i++ { - s := MyStruct{Value: Int32(i)} - x += s.Value.(Int32).Int32() - } - }) - // This type-asserts to an interface type and calls its method. - // v.(BenchValue) is super slow, see https://billglover.me/2018/09/17/how-expensive-is-a-go-function-call/ - // or use `go tool compile -S test.go` for more info. - b.Run("v.(BenchValue).Int32() (interface assert method)", func(b *testing.B) { - var v interface{} = Int32(1) - x := int32(0) - for i := 0; i < b.N; i++ { - x += v.(BenchValue).Int32() - } - }) -} - -// there is a choice between type-switching on a slice of interfaces, or to -// iterate over a slice of super-structs. -func BenchmarkTypeSwitchOrCreate(b *testing.B) { - type Object interface{} - type StructA struct { - Inner Object - A int - B int - } - type StructB struct { - C int - D int - } - x := make([]Object, 1000) - y := make([]StructA, 1000) - for i := 0; i < 1000; i++ { - x[i] = StructA{StructB{0, 0}, 0, 0} - y[i] = StructA{StructB{0, 0}, 0, 0} - } - c := 0 - b.Run("type-switch", func(b *testing.B) { - for j := 0; j < b.N; j++ { - for i := 0; i < 1000; i++ { - switch xi := x[i].(type) { - case StructA: - switch xi.Inner.(type) { - case StructA: - panic("shouldn't happen") - case StructB: - c++ - } - case StructB: - panic("shouldn't happen") - } - } - } - b.Log(c) - }) - b.Run("super-struct", func(b *testing.B) { - for j := 0; j < b.N; j++ { - for i := 0; i < 1000; i++ { - switch y[i].Inner.(type) { - case StructA: - panic("shouldn't happen") - case StructB: - c++ - } - } - } - b.Log(c) - }) -} - -func BenchmarkReflectValueOf(b *testing.B) { - things := []interface{}{ - int(0), - string(""), - struct{}{}, - } - var rv reflect.Value - for _, thing := range things { - b.Run(reflect.TypeOf(thing).String(), func(b *testing.B) { - for i := 0; i < b.N; i++ { - rv = reflect.ValueOf(thing) - } - }) - } - b.Log(rv) -} - -func BenchmarkReflectAddInt64(b *testing.B) { - var rv reflect.Value = reflect.ValueOf(int64(1)) - var x int64 - for i := 0; i < b.N; i++ { - x += rv.Int() - } - b.Log(x) -} - -func BenchmarkNativeAddInt64(b *testing.B) { - var x int64 - for i := 0; i < b.N; i++ { - x += 1 - } - b.Log(x) -} - -func BenchmarkReflectTypeOf(b *testing.B) { - var x int64 - var rt reflect.Type - for i := 0; i < b.N; i++ { - rt = reflect.TypeOf(x) - } - b.Log(x, rt) -} - -func BenchmarkInterfaceEquality(b *testing.B) { - ctr := 0 - var x interface{} - var y interface{} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) -} - -func BenchmarkPointerEquality(b *testing.B) { - ctr := 0 - a := 1 - c := 2 - x := &a - y := &c - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) -} - -func BenchmarkPointerDerefEquality(b *testing.B) { - ctr := 0 - a := 1 - c := 2 - x := &a - y := &c - for i := 0; i < b.N; i++ { - if *x == *y { - ctr++ - } - } - b.Log(ctr) -} - -func BenchmarkArrayEquality(b *testing.B) { - b.Run("ArrayEquality[1]", func(b *testing.B) { - ctr := 0 - x := [1]byte{0x00} - y := [1]byte{0x00} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) - }) - b.Run("ArrayEquality[8]", func(b *testing.B) { - ctr := 0 - x := [8]byte{} - y := [8]byte{} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) - }) - b.Run("ArrayEquality[16]", func(b *testing.B) { - ctr := 0 - x := [16]byte{} - y := [16]byte{} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) - }) - b.Run("ArrayEquality[20]", func(b *testing.B) { - ctr := 0 - x := [20]byte{} - y := [20]byte{} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) - }) - b.Run("ArrayEquality[32]", func(b *testing.B) { - ctr := 0 - x := [32]byte{} - y := [32]byte{} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) - }) - b.Run("ArrayEquality[64]", func(b *testing.B) { - ctr := 0 - x := [64]byte{} - y := [64]byte{} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) - }) - b.Run("ArrayEquality[256]", func(b *testing.B) { - ctr := 0 - x := [256]byte{} - y := [256]byte{} - for i := 0; i < b.N; i++ { - if x == y { - ctr++ - } - } - b.Log(ctr) - }) -} From 02152758bb7d4cfd831648f3a2f1b3a1ced6b359 Mon Sep 17 00:00:00 2001 From: Morgan Bazalgette Date: Fri, 2 Feb 2024 11:58:26 +0100 Subject: [PATCH 2/2] matrix_mult -> matrix, as it is more representative --- gnovm/pkg/gnolang/benchdata/{matrix_mult.gno => matrix.gno} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename gnovm/pkg/gnolang/benchdata/{matrix_mult.gno => matrix.gno} (100%) diff --git a/gnovm/pkg/gnolang/benchdata/matrix_mult.gno b/gnovm/pkg/gnolang/benchdata/matrix.gno similarity index 100% rename from gnovm/pkg/gnolang/benchdata/matrix_mult.gno rename to gnovm/pkg/gnolang/benchdata/matrix.gno