Files
wazero/internal/engine/compiler/compiler_initialization_test.go
Takeshi Yoneda 9ad8af121a compiler: simplify calling convention (#782)
This simplifies the calling convention and consolidates the call frame stack
and value stack into a single stack.

As a result, the cost of function calls decreases because we now don't need
to check the boundary twice (value and call frame stacks) at each function call.

The following is the result of the benchmark for recursive Fibonacci
function in integration_test/bench/testdata/case.go, and it shows that
this actually improves the performance of function calls.

[amd64]
name                               old time/op  new time/op  delta
Invocation/compiler/fib_for_5-32    109ns ± 3%    81ns ± 1%  -25.86%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_10-32   556ns ± 3%   473ns ± 3%  -14.99%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_20-32  61.4µs ± 2%  55.9µs ± 5%   -8.98%  (p=0.008 n=5+5)
Invocation/compiler/fib_for_30-32  7.41ms ± 3%  6.83ms ± 3%   -7.90%  (p=0.008 n=5+5)


[arm64]
name                               old time/op    new time/op    delta
Invocation/compiler/fib_for_5-10     67.7ns ± 1%    60.2ns ± 1%  -11.12%  (p=0.000 n=9+9)
Invocation/compiler/fib_for_10-10     487ns ± 1%     460ns ± 0%   -5.56%  (p=0.000 n=10+9)
Invocation/compiler/fib_for_20-10    58.0µs ± 1%    54.3µs ± 1%   -6.38%  (p=0.000 n=10+10)
Invocation/compiler/fib_for_30-10    7.12ms ± 1%    6.67ms ± 1%   -6.31%  (p=0.000 n=10+9)

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
2022-09-06 13:29:56 +09:00

265 lines
9.2 KiB
Go

package compiler
import (
"fmt"
"reflect"
"testing"
"unsafe"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wazeroir"
)
func TestCompiler_compileModuleContextInitialization(t *testing.T) {
tests := []struct {
name string
moduleInstance *wasm.ModuleInstance
}{
{
name: "no nil",
moduleInstance: &wasm.ModuleInstance{
Globals: []*wasm.GlobalInstance{{Val: 100}},
Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
Tables: []*wasm.TableInstance{
{References: make([]wasm.Reference, 20)},
{References: make([]wasm.Reference, 10)},
},
TypeIDs: make([]wasm.FunctionTypeID, 10),
DataInstances: make([][]byte, 10),
ElementInstances: make([]wasm.ElementInstance, 10),
},
},
{
name: "element instances nil",
moduleInstance: &wasm.ModuleInstance{
Globals: []*wasm.GlobalInstance{{Val: 100}},
Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
Tables: []*wasm.TableInstance{{References: make([]wasm.Reference, 20)}},
TypeIDs: make([]wasm.FunctionTypeID, 10),
DataInstances: make([][]byte, 10),
ElementInstances: nil,
},
},
{
name: "data instances nil",
moduleInstance: &wasm.ModuleInstance{
Globals: []*wasm.GlobalInstance{{Val: 100}},
Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
Tables: []*wasm.TableInstance{{References: make([]wasm.Reference, 20)}},
TypeIDs: make([]wasm.FunctionTypeID, 10),
DataInstances: nil,
ElementInstances: make([]wasm.ElementInstance, 10),
},
},
{
name: "globals nil",
moduleInstance: &wasm.ModuleInstance{
Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
Tables: []*wasm.TableInstance{{References: make([]wasm.Reference, 20)}},
TypeIDs: make([]wasm.FunctionTypeID, 10),
DataInstances: make([][]byte, 10),
ElementInstances: make([]wasm.ElementInstance, 10),
},
},
{
name: "memory nil",
moduleInstance: &wasm.ModuleInstance{
Globals: []*wasm.GlobalInstance{{Val: 100}},
Tables: []*wasm.TableInstance{{References: make([]wasm.Reference, 20)}},
TypeIDs: make([]wasm.FunctionTypeID, 10),
DataInstances: make([][]byte, 10),
ElementInstances: make([]wasm.ElementInstance, 10),
},
},
{
name: "table nil",
moduleInstance: &wasm.ModuleInstance{
Memory: &wasm.MemoryInstance{Buffer: make([]byte, 10)},
Tables: []*wasm.TableInstance{{References: nil}},
TypeIDs: make([]wasm.FunctionTypeID, 10),
DataInstances: make([][]byte, 10),
ElementInstances: make([]wasm.ElementInstance, 10),
},
},
{
name: "table empty",
moduleInstance: &wasm.ModuleInstance{
Tables: []*wasm.TableInstance{{References: make([]wasm.Reference, 20)}},
TypeIDs: make([]wasm.FunctionTypeID, 10),
DataInstances: make([][]byte, 10),
ElementInstances: make([]wasm.ElementInstance, 10),
},
},
{
name: "memory zero length",
moduleInstance: &wasm.ModuleInstance{
Memory: &wasm.MemoryInstance{Buffer: make([]byte, 0)},
},
},
{
name: "all nil except mod engine",
moduleInstance: &wasm.ModuleInstance{},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
env := newCompilerEnvironment()
env.moduleInstance = tc.moduleInstance
ce := env.callEngine()
ir := &wazeroir.CompilationResult{
HasMemory: tc.moduleInstance.Memory != nil,
HasTable: len(tc.moduleInstance.Tables) > 0,
HasDataInstances: len(tc.moduleInstance.DataInstances) > 0,
HasElementInstances: len(tc.moduleInstance.ElementInstances) > 0,
}
for _, g := range tc.moduleInstance.Globals {
ir.Globals = append(ir.Globals, g.Type)
}
compiler := env.requireNewCompiler(t, newCompiler, ir)
me := &moduleEngine{functions: make([]*function, 10)}
tc.moduleInstance.Engine = me
err := compiler.compileModuleContextInitialization()
require.NoError(t, err)
require.Zero(t, len(compiler.runtimeValueLocationStack().usedRegisters), "expected no usedRegisters")
compiler.compileExitFromNativeCode(nativeCallStatusCodeReturned)
// Generate the code under test.
code, _, err := compiler.compile()
require.NoError(t, err)
env.exec(code)
// Check the exit status.
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
// Check if the fields of callEngine.moduleContext are updated.
bufSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.Globals))
require.Equal(t, bufSliceHeader.Data, ce.moduleContext.globalElement0Address)
if tc.moduleInstance.Memory != nil {
bufSliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.Memory.Buffer))
require.Equal(t, uint64(bufSliceHeader.Len), ce.moduleContext.memorySliceLen)
require.Equal(t, bufSliceHeader.Data, ce.moduleContext.memoryElement0Address)
require.Equal(t, tc.moduleInstance.Memory, ce.moduleContext.memoryInstance)
}
if len(tc.moduleInstance.Tables) > 0 {
tableHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.Tables))
require.Equal(t, tableHeader.Data, ce.moduleContext.tablesElement0Address)
require.Equal(t, uintptr(unsafe.Pointer(&tc.moduleInstance.TypeIDs[0])), ce.moduleContext.typeIDsElement0Address)
require.Equal(t, uintptr(unsafe.Pointer(&tc.moduleInstance.Tables[0])), ce.moduleContext.tablesElement0Address)
}
if len(tc.moduleInstance.DataInstances) > 0 {
dataInstancesHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.DataInstances))
require.Equal(t, dataInstancesHeader.Data, ce.moduleContext.dataInstancesElement0Address)
require.Equal(t, uintptr(unsafe.Pointer(&tc.moduleInstance.DataInstances[0])), ce.moduleContext.dataInstancesElement0Address)
}
if len(tc.moduleInstance.ElementInstances) > 0 {
elementInstancesHeader := (*reflect.SliceHeader)(unsafe.Pointer(&tc.moduleInstance.ElementInstances))
require.Equal(t, elementInstancesHeader.Data, ce.moduleContext.elementInstancesElement0Address)
require.Equal(t, uintptr(unsafe.Pointer(&tc.moduleInstance.ElementInstances[0])), ce.moduleContext.elementInstancesElement0Address)
}
require.Equal(t, uintptr(unsafe.Pointer(&me.functions[0])), ce.moduleContext.functionsElement0Address)
})
}
}
func TestCompiler_compileMaybeGrowStack(t *testing.T) {
t.Run("not grow", func(t *testing.T) {
const stackPointerCeil = 5
for _, baseOffset := range []uint64{5, 10, 20} {
t.Run(fmt.Sprintf("%d", baseOffset), func(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, newCompiler, nil)
err := compiler.compilePreamble()
require.NoError(t, err)
require.NotNil(t, compiler.getOnStackPointerCeilDeterminedCallBack())
stackLen := uint64(len(env.stack()))
stackBasePointer := stackLen - baseOffset // Ceil <= stackLen - stackBasePointer = no need to grow!
compiler.getOnStackPointerCeilDeterminedCallBack()(stackPointerCeil)
env.setStackBasePointer(stackBasePointer)
compiler.compileExitFromNativeCode(nativeCallStatusCodeReturned)
// Generate and run the code under test.
code, _, err := compiler.compile()
require.NoError(t, err)
env.exec(code)
// The status code must be "Returned", not "BuiltinFunctionCall".
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
})
}
})
var defaultStackLen = uint64(initialStackSize)
t.Run("grow", func(t *testing.T) {
tests := []struct {
name string
stackPointerCeil uint64
stackBasePointer uint64
}{
{
name: "ceil=6/sbp=len-5",
stackPointerCeil: 6,
stackBasePointer: defaultStackLen - 5,
},
{
name: "ceil=10000/sbp=0",
stackPointerCeil: 10000,
stackBasePointer: 0,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
env := newCompilerEnvironment()
compiler := env.requireNewCompiler(t, newCompiler, nil)
err := compiler.compilePreamble()
require.NoError(t, err)
// On the return from grow value stack, we simply return.
err = compiler.compileReturnFunction()
require.NoError(t, err)
// Generate code under test with the given stackPointerCeil.
compiler.setStackPointerCeil(tc.stackPointerCeil)
code, _, err := compiler.compile()
require.NoError(t, err)
// And run the code with the specified stackBasePointer.
env.setStackBasePointer(tc.stackBasePointer)
env.exec(code)
// Check if the call exits with builtin function call status.
require.Equal(t, nativeCallStatusCodeCallBuiltInFunction, env.compilerStatus())
// Reenter from the return address.
returnAddress := env.ce.returnAddress
require.True(t, returnAddress != 0, "returnAddress was zero %d", returnAddress)
nativecall(
returnAddress, uintptr(unsafe.Pointer(env.callEngine())),
uintptr(unsafe.Pointer(env.module())),
)
// Check the result. This should be "Returned".
require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus())
})
}
})
}