Files
wazero/internal/integration_test/bench/hostfunc_bench_test.go
2023-05-01 08:52:40 +09:00

240 lines
6.7 KiB
Go

package bench
import (
"context"
_ "embed"
"encoding/binary"
"math"
"testing"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/engine/compiler"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
)
const (
// callGoHostName is the name of exported function which calls the
// Go-implemented host function.
callGoHostName = "call_go_host"
// callGoReflectHostName is the name of exported function which calls the
// Go-implemented host function defined in reflection.
callGoReflectHostName = "call_go_reflect_host"
)
// BenchmarkHostFunctionCall measures the cost of host function calls whose target functions are either
// Go-implemented or Wasm-implemented, and compare the results between them.
func BenchmarkHostFunctionCall(b *testing.B) {
if !platform.CompilerSupported() {
b.Skip()
}
m := setupHostCallBench(func(err error) {
if err != nil {
b.Fatal(err)
}
})
const offset = uint64(100)
const val = float32(1.1234)
binary.LittleEndian.PutUint32(m.MemoryInstance.Buffer[offset:], math.Float32bits(val))
for _, fn := range []string{callGoReflectHostName, callGoHostName} {
fn := fn
b.Run(fn, func(b *testing.B) {
ce := getCallEngine(m, fn)
b.ResetTimer()
for i := 0; i < b.N; i++ {
res, err := ce.Call(testCtx, offset)
if err != nil {
b.Fatal(err)
}
if uint32(res[0]) != math.Float32bits(val) {
b.Fail()
}
}
})
b.Run(fn+"_with_stack", func(b *testing.B) {
ce := getCallEngine(m, fn)
b.ResetTimer()
stack := make([]uint64, 1)
for i := 0; i < b.N; i++ {
stack[0] = offset
err := ce.CallWithStack(testCtx, stack)
if err != nil {
b.Fatal(err)
}
if uint32(stack[0]) != math.Float32bits(val) {
b.Fail()
}
}
})
}
}
func TestBenchmarkFunctionCall(t *testing.T) {
if !platform.CompilerSupported() {
t.Skip()
}
m := setupHostCallBench(func(err error) {
require.NoError(t, err)
})
callGoHost := getCallEngine(m, callGoHostName)
callGoReflectHost := getCallEngine(m, callGoReflectHostName)
require.NotNil(t, callGoHost)
require.NotNil(t, callGoReflectHost)
tests := []struct {
offset uint32
val float32
}{
{offset: 0, val: math.Float32frombits(0xffffffff)},
{offset: 100, val: 1.12314},
{offset: wasm.MemoryPageSize - 4, val: 1.12314},
}
mem := m.MemoryInstance.Buffer
for _, f := range []struct {
name string
ce api.Function
}{
{name: "go", ce: callGoHost},
{name: "go-reflect", ce: callGoReflectHost},
} {
f := f
t.Run(f.name, func(t *testing.T) {
for _, tc := range tests {
binary.LittleEndian.PutUint32(mem[tc.offset:], math.Float32bits(tc.val))
res, err := f.ce.Call(context.Background(), uint64(tc.offset))
require.NoError(t, err)
require.Equal(t, math.Float32bits(tc.val), uint32(res[0]))
}
})
}
}
func getCallEngine(m *wasm.ModuleInstance, name string) (ce api.Function) {
exp := m.Exports[name]
ce = m.Engine.NewFunction(exp.Index)
return
}
func setupHostCallBench(requireNoError func(error)) *wasm.ModuleInstance {
eng := compiler.NewEngine(context.Background(), api.CoreFeaturesV2, nil)
ft := wasm.FunctionType{
Params: []wasm.ValueType{wasm.ValueTypeI32},
Results: []wasm.ValueType{wasm.ValueTypeF32},
ParamNumInUint64: 1, ResultNumInUint64: 1,
}
// Build the host module.
hostModule := &wasm.Module{
TypeSection: []wasm.FunctionType{ft},
FunctionSection: []wasm.Index{0, 0},
CodeSection: []wasm.Code{
{
GoFunc: api.GoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) {
ret, ok := mod.Memory().ReadUint32Le(uint32(stack[0]))
if !ok {
panic("couldn't read memory")
}
stack[0] = uint64(ret)
}),
},
wasm.MustParseGoReflectFuncCode(
func(_ context.Context, m api.Module, pos uint32) float32 {
ret, ok := m.Memory().ReadUint32Le(pos)
if !ok {
panic("couldn't read memory")
}
return math.Float32frombits(ret)
},
),
},
ExportSection: []wasm.Export{
{Name: "go", Type: wasm.ExternTypeFunc, Index: 0},
{Name: "go-reflect", Type: wasm.ExternTypeFunc, Index: 1},
},
Exports: map[string]*wasm.Export{
"go": {Name: "go", Type: wasm.ExternTypeFunc, Index: 0},
"go-reflect": {Name: "go-reflect", Type: wasm.ExternTypeFunc, Index: 1},
},
ID: wasm.ModuleID{1, 2, 3, 4, 5},
}
hostModule.BuildFunctionDefinitions()
host := &wasm.ModuleInstance{
ModuleName: "host", TypeIDs: []wasm.FunctionTypeID{0},
Definitions: hostModule.FunctionDefinitionSection,
}
host.Exports = hostModule.Exports
err := eng.CompileModule(testCtx, hostModule, nil, false)
requireNoError(err)
hostMe, err := eng.NewModuleEngine(hostModule, host)
requireNoError(err)
linkModuleToEngine(host, hostMe)
// Build the importing module.
importingModule := &wasm.Module{
ImportFunctionCount: 2,
TypeSection: []wasm.FunctionType{ft},
ImportSection: []wasm.Import{
// Placeholders for imports from hostModule.
{Type: wasm.ExternTypeFunc},
{Type: wasm.ExternTypeFunc},
},
FunctionSection: []wasm.Index{0, 0},
ExportSection: []wasm.Export{
{Name: callGoHostName, Type: wasm.ExternTypeFunc, Index: 2},
{Name: callGoReflectHostName, Type: wasm.ExternTypeFunc, Index: 3},
},
Exports: map[string]*wasm.Export{
callGoHostName: {Name: callGoHostName, Type: wasm.ExternTypeFunc, Index: 2},
callGoReflectHostName: {Name: callGoReflectHostName, Type: wasm.ExternTypeFunc, Index: 3},
},
CodeSection: []wasm.Code{
{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Calling the index 0 = host.go.
{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, // Calling the index 1 = host.go-reflect.
},
// Indicates that this module has a memory so that compilers are able to assemble memory-related initialization.
MemorySection: &wasm.Memory{Min: 1},
ID: wasm.ModuleID{1},
}
importingModule.BuildFunctionDefinitions()
err = eng.CompileModule(testCtx, importingModule, nil, false)
requireNoError(err)
importing := &wasm.ModuleInstance{
TypeIDs: []wasm.FunctionTypeID{0},
Definitions: importingModule.FunctionDefinitionSection,
}
importing.Exports = importingModule.Exports
importingMe, err := eng.NewModuleEngine(importingModule, importing)
requireNoError(err)
linkModuleToEngine(importing, importingMe)
importingMe.ResolveImportedFunction(0, 0, hostMe)
importingMe.ResolveImportedFunction(1, 1, hostMe)
importing.MemoryInstance = &wasm.MemoryInstance{Buffer: make([]byte, wasm.MemoryPageSize), Min: 1, Cap: 1, Max: 1}
return importing
}
func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) {
module.Engine = me
}