Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ coverage:

benchmark:
@go test -run="-" -bench=".*" -benchmem $(test-options) ./...
@(cd benchmarks && go test -run="-" -bench=".*" -benchmem $(test-options) ./...)

lint: fmt vet

Expand Down
196 changes: 196 additions & 0 deletions benchmarks/fib_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package bench

import (
"context"
"strings"
"testing"

tengo "github.com/d5/tengo/v2"
"github.com/dop251/goja"
lua "github.com/yuin/gopher-lua"

"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"

"github.com/siyul-park/minivm/interp"
)

// fibN is the input for the cross-runtime comparison.
// fib(35) = 9_227_465; ~29.8 M recursive calls; no memoisation.
const fibN int32 = 35

// fibWasm is a precomputed WASM binary for the recursive fib function:
//
// (module (func $fib (export "fib") (param i32) (result i32)
// local.get 0 i32.const 2 i32.lt_s
// if (result i32) local.get 0
// else
// local.get 0 i32.const 1 i32.sub call $fib
// local.get 0 i32.const 2 i32.sub call $fib i32.add
// end))
var fibWasm = []byte{
// magic + version
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
// type section: (i32) -> i32
0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f,
// function section: 1 function of type 0
0x03, 0x02, 0x01, 0x00,
// export section: "fib" as function 0
0x07, 0x07, 0x01, 0x03, 0x66, 0x69, 0x62, 0x00, 0x00,
// code section: 1 function body (28 bytes, 0 locals)
0x0a, 0x1e, 0x01, 0x1c, 0x00,
// body
0x20, 0x00, // local.get 0
0x41, 0x02, // i32.const 2
0x48, // i32.lt_s
0x04, 0x7f, // if (result i32)
0x20, 0x00, // local.get 0 — then branch
0x05, // else
0x20, 0x00, // local.get 0
0x41, 0x01, // i32.const 1
0x6b, // i32.sub
0x10, 0x00, // call 0
0x20, 0x00, // local.get 0
0x41, 0x02, // i32.const 2
0x6b, // i32.sub
0x10, 0x00, // call 0
0x6a, // i32.add — else branch end
0x0b, // end (if/else)
0x0b, // end (function)
}

// fibGo is a pure-Go recursive fib used as the native baseline.
func fibGo(n int32) int32 {
if n < 2 {
return n
}
return fibGo(n-1) + fibGo(n-2)
}

// BenchmarkFib35 compares recursive fib(35) across six runtimes.
// Each sub-benchmark creates its runtime and compiles its program once
// outside the timed loop, then calls fib(35) b.N times.
func BenchmarkFib35(b *testing.B) {
b.Run("native", func(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
fibGo(fibN)
}
})

b.Run("minivm", func(b *testing.B) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

prog := Fib(fibN)
i := interp.New(prog)
defer i.Close()

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
_ = i.Run(ctx)
i.Reset()
}
})

b.Run("wazero", func(b *testing.B) {
ctx := context.Background()

r := wazero.NewRuntime(ctx)
defer r.Close(ctx)

mod, err := r.Instantiate(ctx, fibWasm)
if err != nil {
b.Fatal(err)
}
fib := mod.ExportedFunction("fib")

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
if _, err := fib.Call(ctx, api.EncodeI32(fibN)); err != nil {
b.Fatal(err)
}
}
})

b.Run("tengo", func(b *testing.B) {
src := `
fib := func(n) {
if n < 2 { return n }
return fib(n-1) + fib(n-2)
}
result := fib(35)
`
s := tengo.NewScript([]byte(src))
compiled, err := s.Compile()
if err != nil {
b.Fatal(err)
}

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
cloned := compiled.Clone()
if err := cloned.Run(); err != nil {
b.Fatal(err)
}
}
})

b.Run("gopher_lua", func(b *testing.B) {
L := lua.NewState()
defer L.Close()

// Define fib as a global so it can call itself by name.
if err := L.DoString(`
fib = function(n)
if n < 2 then return n end
return fib(n-1) + fib(n-2)
end
`); err != nil {
b.Fatal(err)
}
fibFn := L.GetGlobal("fib")

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
if err := L.CallByParam(lua.P{
Fn: fibFn,
NRet: 1,
Protect: true,
}, lua.LNumber(fibN)); err != nil {
b.Fatal(err)
}
L.Pop(1)
}
})

b.Run("goja", func(b *testing.B) {
vm := goja.New()
p, err := goja.Compile("fib", strings.TrimSpace(`
function fib(n) {
if (n < 2) return n;
return fib(n-1) + fib(n-2);
}
`), true)
if err != nil {
b.Fatal(err)
}
if _, err := vm.RunProgram(p); err != nil {
b.Fatal(err)
}
fibFn, _ := goja.AssertFunction(vm.Get("fib"))

b.ReportAllocs()
b.ResetTimer()
for n := 0; n < b.N; n++ {
if _, err := fibFn(goja.Undefined(), vm.ToValue(fibN)); err != nil {
b.Fatal(err)
}
}
})
}
21 changes: 21 additions & 0 deletions benchmarks/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module github.com/siyul-park/minivm/benchmarks

go 1.26.2

require (
github.com/d5/tengo/v2 v2.17.0
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
github.com/siyul-park/minivm v0.0.0
github.com/tetratelabs/wazero v1.11.0
github.com/yuin/gopher-lua v1.1.2
)

require (
github.com/dlclark/regexp2 v1.11.4 // indirect
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.3.8 // indirect
)

replace github.com/siyul-park/minivm => ../
30 changes: 30 additions & 0 deletions benchmarks/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/d5/tengo/v2 v2.17.0 h1:BWUN9NoJzw48jZKiYDXDIF3QrIVZRm1uV1gTzeZ2lqM=
github.com/d5/tengo/v2 v2.17.0/go.mod h1:XRGjEs5I9jYIKTxly6HCF8oiiilk5E/RYXOZ5b0DZC8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk=
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA=
github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU=
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
55 changes: 55 additions & 0 deletions benchmarks/programs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Package bench provides reusable benchmark programs for minivm.
//
// This package is a separate Go module so that the main minivm packages carry
// no dependency on benchmark infrastructure.
package bench

import (
"github.com/siyul-park/minivm/instr"
"github.com/siyul-park/minivm/program"
"github.com/siyul-park/minivm/types"
)

// Fib returns a program that computes fibonacci(n) via recursive descent.
// The function is self-referential: it pushes itself via CONST_GET and calls
// recursively. fib(35) = 9_227_465 with ~29.8 M recursive calls.
//
// BR_IF operands are byte offsets from the branch instruction start, matching
// the threaded compiler's encoding (see interp/threaded.go).
func Fib(n int32) *program.Program {
fibFn := types.NewFunctionBuilder(&types.FunctionType{
Params: []types.Type{types.TypeI32},
Returns: []types.Type{types.TypeI32},
}).Emit(
// if n < 2 { return n }
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.I32_CONST, 2),
instr.New(instr.I32_LT_S),
instr.New(instr.BR_IF, 26), // jump forward 26 bytes to base-case LOCAL_GET
// return fib(n-1) + fib(n-2)
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.I32_CONST, 1),
instr.New(instr.I32_SUB),
instr.New(instr.CONST_GET, 0),
instr.New(instr.CALL),
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.I32_CONST, 2),
instr.New(instr.I32_SUB),
instr.New(instr.CONST_GET, 0),
instr.New(instr.CALL),
instr.New(instr.I32_ADD),
instr.New(instr.RETURN),
// base case: return n
instr.New(instr.LOCAL_GET, 0),
instr.New(instr.RETURN),
).Build()

return program.New(
[]instr.Instruction{
instr.New(instr.I32_CONST, uint64(n)),
instr.New(instr.CONST_GET, 0),
instr.New(instr.CALL),
},
program.WithConstants(fibFn),
)
}
Loading
Loading