github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/internal/integration_test/bench/hostfunc_bench_test.go (about)

     1  package bench
     2  
     3  import (
     4  	"context"
     5  	_ "embed"
     6  	"encoding/binary"
     7  	"math"
     8  	"testing"
     9  
    10  	"github.com/bananabytelabs/wazero/api"
    11  	"github.com/bananabytelabs/wazero/internal/engine/compiler"
    12  	"github.com/bananabytelabs/wazero/internal/platform"
    13  	"github.com/bananabytelabs/wazero/internal/testing/require"
    14  	"github.com/bananabytelabs/wazero/internal/wasm"
    15  )
    16  
    17  const (
    18  	// callGoHostName is the name of exported function which calls the
    19  	// Go-implemented host function.
    20  	callGoHostName = "call_go_host"
    21  	// callGoReflectHostName is the name of exported function which calls the
    22  	// Go-implemented host function defined in reflection.
    23  	callGoReflectHostName = "call_go_reflect_host"
    24  )
    25  
    26  // BenchmarkHostFunctionCall measures the cost of host function calls whose target functions are either
    27  // Go-implemented or Wasm-implemented, and compare the results between them.
    28  func BenchmarkHostFunctionCall(b *testing.B) {
    29  	if !platform.CompilerSupported() {
    30  		b.Skip()
    31  	}
    32  
    33  	m := setupHostCallBench(func(err error) {
    34  		if err != nil {
    35  			b.Fatal(err)
    36  		}
    37  	})
    38  
    39  	const offset = uint64(100)
    40  	const val = float32(1.1234)
    41  
    42  	binary.LittleEndian.PutUint32(m.MemoryInstance.Buffer[offset:], math.Float32bits(val))
    43  
    44  	for _, fn := range []string{callGoReflectHostName, callGoHostName} {
    45  		fn := fn
    46  
    47  		b.Run(fn, func(b *testing.B) {
    48  			ce := getCallEngine(m, fn)
    49  
    50  			b.ResetTimer()
    51  			for i := 0; i < b.N; i++ {
    52  				res, err := ce.Call(testCtx, offset)
    53  				if err != nil {
    54  					b.Fatal(err)
    55  				}
    56  				if uint32(res[0]) != math.Float32bits(val) {
    57  					b.Fail()
    58  				}
    59  			}
    60  		})
    61  
    62  		b.Run(fn+"_with_stack", func(b *testing.B) {
    63  			ce := getCallEngine(m, fn)
    64  
    65  			b.ResetTimer()
    66  			stack := make([]uint64, 1)
    67  			for i := 0; i < b.N; i++ {
    68  				stack[0] = offset
    69  				err := ce.CallWithStack(testCtx, stack)
    70  				if err != nil {
    71  					b.Fatal(err)
    72  				}
    73  				if uint32(stack[0]) != math.Float32bits(val) {
    74  					b.Fail()
    75  				}
    76  			}
    77  		})
    78  	}
    79  }
    80  
    81  func TestBenchmarkFunctionCall(t *testing.T) {
    82  	if !platform.CompilerSupported() {
    83  		t.Skip()
    84  	}
    85  
    86  	m := setupHostCallBench(func(err error) {
    87  		require.NoError(t, err)
    88  	})
    89  
    90  	callGoHost := getCallEngine(m, callGoHostName)
    91  	callGoReflectHost := getCallEngine(m, callGoReflectHostName)
    92  
    93  	require.NotNil(t, callGoHost)
    94  	require.NotNil(t, callGoReflectHost)
    95  
    96  	tests := []struct {
    97  		offset uint32
    98  		val    float32
    99  	}{
   100  		{offset: 0, val: math.Float32frombits(0xffffffff)},
   101  		{offset: 100, val: 1.12314},
   102  		{offset: wasm.MemoryPageSize - 4, val: 1.12314},
   103  	}
   104  
   105  	mem := m.MemoryInstance.Buffer
   106  
   107  	for _, f := range []struct {
   108  		name string
   109  		ce   api.Function
   110  	}{
   111  		{name: "go", ce: callGoHost},
   112  		{name: "go-reflect", ce: callGoReflectHost},
   113  	} {
   114  		f := f
   115  		t.Run(f.name, func(t *testing.T) {
   116  			for _, tc := range tests {
   117  				binary.LittleEndian.PutUint32(mem[tc.offset:], math.Float32bits(tc.val))
   118  				res, err := f.ce.Call(context.Background(), uint64(tc.offset))
   119  				require.NoError(t, err)
   120  				require.Equal(t, math.Float32bits(tc.val), uint32(res[0]))
   121  			}
   122  		})
   123  	}
   124  }
   125  
   126  func getCallEngine(m *wasm.ModuleInstance, name string) (ce api.Function) {
   127  	exp := m.Exports[name]
   128  	ce = m.Engine.NewFunction(exp.Index)
   129  	return
   130  }
   131  
   132  func setupHostCallBench(requireNoError func(error)) *wasm.ModuleInstance {
   133  	eng := compiler.NewEngine(context.Background(), api.CoreFeaturesV2, nil)
   134  
   135  	ft := wasm.FunctionType{
   136  		Params:           []wasm.ValueType{wasm.ValueTypeI32},
   137  		Results:          []wasm.ValueType{wasm.ValueTypeF32},
   138  		ParamNumInUint64: 1, ResultNumInUint64: 1,
   139  	}
   140  
   141  	// Build the host module.
   142  	hostModule := &wasm.Module{
   143  		TypeSection:     []wasm.FunctionType{ft},
   144  		FunctionSection: []wasm.Index{0, 0},
   145  		CodeSection: []wasm.Code{
   146  			{
   147  				GoFunc: api.GoModuleFunc(func(_ context.Context, mod api.Module, stack []uint64) {
   148  					ret, ok := mod.Memory().ReadUint32Le(uint32(stack[0]))
   149  					if !ok {
   150  						panic("couldn't read memory")
   151  					}
   152  					stack[0] = uint64(ret)
   153  				}),
   154  			},
   155  			wasm.MustParseGoReflectFuncCode(
   156  				func(_ context.Context, m api.Module, pos uint32) float32 {
   157  					ret, ok := m.Memory().ReadUint32Le(pos)
   158  					if !ok {
   159  						panic("couldn't read memory")
   160  					}
   161  					return math.Float32frombits(ret)
   162  				},
   163  			),
   164  		},
   165  		ExportSection: []wasm.Export{
   166  			{Name: "go", Type: wasm.ExternTypeFunc, Index: 0},
   167  			{Name: "go-reflect", Type: wasm.ExternTypeFunc, Index: 1},
   168  		},
   169  		Exports: map[string]*wasm.Export{
   170  			"go":         {Name: "go", Type: wasm.ExternTypeFunc, Index: 0},
   171  			"go-reflect": {Name: "go-reflect", Type: wasm.ExternTypeFunc, Index: 1},
   172  		},
   173  		ID: wasm.ModuleID{1, 2, 3, 4, 5},
   174  	}
   175  
   176  	host := &wasm.ModuleInstance{ModuleName: "host", TypeIDs: []wasm.FunctionTypeID{0}}
   177  	host.Exports = hostModule.Exports
   178  
   179  	err := eng.CompileModule(testCtx, hostModule, nil, false)
   180  	requireNoError(err)
   181  
   182  	hostMe, err := eng.NewModuleEngine(hostModule, host)
   183  	requireNoError(err)
   184  	linkModuleToEngine(host, hostMe)
   185  
   186  	// Build the importing module.
   187  	importingModule := &wasm.Module{
   188  		ImportFunctionCount: 2,
   189  		TypeSection:         []wasm.FunctionType{ft},
   190  		ImportSection: []wasm.Import{
   191  			// Placeholders for imports from hostModule.
   192  			{Type: wasm.ExternTypeFunc},
   193  			{Type: wasm.ExternTypeFunc},
   194  		},
   195  		FunctionSection: []wasm.Index{0, 0},
   196  		ExportSection: []wasm.Export{
   197  			{Name: callGoHostName, Type: wasm.ExternTypeFunc, Index: 2},
   198  			{Name: callGoReflectHostName, Type: wasm.ExternTypeFunc, Index: 3},
   199  		},
   200  		Exports: map[string]*wasm.Export{
   201  			callGoHostName:        {Name: callGoHostName, Type: wasm.ExternTypeFunc, Index: 2},
   202  			callGoReflectHostName: {Name: callGoReflectHostName, Type: wasm.ExternTypeFunc, Index: 3},
   203  		},
   204  		CodeSection: []wasm.Code{
   205  			{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, // Calling the index 0 = host.go.
   206  			{Body: []byte{wasm.OpcodeLocalGet, 0, wasm.OpcodeCall, 1, wasm.OpcodeEnd}}, // Calling the index 1 = host.go-reflect.
   207  		},
   208  		// Indicates that this module has a memory so that compilers are able to assemble memory-related initialization.
   209  		MemorySection: &wasm.Memory{Min: 1},
   210  		ID:            wasm.ModuleID{1},
   211  	}
   212  
   213  	err = eng.CompileModule(testCtx, importingModule, nil, false)
   214  	requireNoError(err)
   215  
   216  	importing := &wasm.ModuleInstance{TypeIDs: []wasm.FunctionTypeID{0}}
   217  	importing.Exports = importingModule.Exports
   218  
   219  	importingMe, err := eng.NewModuleEngine(importingModule, importing)
   220  	requireNoError(err)
   221  	linkModuleToEngine(importing, importingMe)
   222  	importingMe.ResolveImportedFunction(0, 0, hostMe)
   223  	importingMe.ResolveImportedFunction(1, 1, hostMe)
   224  
   225  	importing.MemoryInstance = &wasm.MemoryInstance{Buffer: make([]byte, wasm.MemoryPageSize), Min: 1, Cap: 1, Max: 1}
   226  	return importing
   227  }
   228  
   229  func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) {
   230  	module.Engine = me
   231  }