Avoides allocation with constExpr execution (#1257)

Previously executConstExpr is called various places, and therefore it was necessary to return interface{}
and that resulted in allocation per call.
This commit avoids the allocation by adding executConstExprI32 which is used by data segment manipulation,
and repurposes executConstExpr solely to global instance initialization.

As a result, this completely removes the allocation around const expr execution, and hence the perf improvement
in the instantiation phrase. The improvement diff is proportionate to the number of data segments and globals.

Signed-off-by: Takeshi Yoneda <takeshi@tetrate.io>
This commit is contained in:
Takeshi Yoneda
2023-03-19 20:56:25 -07:00
committed by GitHub
parent 874fb5c8c1
commit fe8784996c
3 changed files with 82 additions and 113 deletions

View File

@@ -591,26 +591,7 @@ func (m *Module) buildGlobals(importedGlobals []*GlobalInstance, funcRefResolver
for i := range m.GlobalSection {
gs := &m.GlobalSection[i]
g := &GlobalInstance{Type: gs.Type}
switch v := executeConstExpression(importedGlobals, &gs.Init).(type) {
case uint32:
if gs.Type.ValType == ValueTypeFuncref {
g.Val = uint64(funcRefResolver(v))
} else {
g.Val = uint64(v)
}
case int32:
g.Val = uint64(uint32(v))
case int64:
g.Val = uint64(v)
case float32:
g.Val = api.EncodeF32(v)
case float64:
g.Val = api.EncodeF64(v)
case [2]uint64:
g.Val, g.ValHi = v[0], v[1]
default:
panic(fmt.Errorf("BUG: invalid conversion %d", v))
}
g.initialize(importedGlobals, &gs.Init, funcRefResolver)
globals[i] = g
}
return

View File

@@ -7,7 +7,6 @@ import (
"sync"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/ieee754"
"github.com/tetratelabs/wazero/internal/leb128"
internalsys "github.com/tetratelabs/wazero/internal/sys"
"github.com/tetratelabs/wazero/sys"
@@ -220,7 +219,7 @@ func (m *ModuleInstance) validateData(data []DataSegment) (err error) {
for i := range data {
d := &data[i]
if !d.IsPassive() {
offset := int(executeConstExpression(m.Globals, &d.OffsetExpression).(int32))
offset := int(executeConstExpressionI32(m.Globals, &d.OffsetExpression))
ceil := offset + len(d.Init)
if offset < 0 || ceil > len(m.Memory.Buffer) {
return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i)
@@ -239,7 +238,7 @@ func (m *ModuleInstance) applyData(data []DataSegment) error {
d := &data[i]
m.DataInstances[i] = d.Init
if !d.IsPassive() {
offset := executeConstExpression(m.Globals, &d.OffsetExpression).(int32)
offset := executeConstExpressionI32(m.Globals, &d.OffsetExpression)
if offset < 0 || int(offset)+len(d.Init) > len(m.Memory.Buffer) {
return fmt.Errorf("%s[%d]: out of bounds memory access", SectionIDName(SectionIDData), i)
}
@@ -516,51 +515,68 @@ func errorInvalidImport(i *Import, idx int, err error) error {
return fmt.Errorf("import[%d] %s[%s.%s]: %w", idx, ExternTypeName(i.Type), i.Module, i.Name, err)
}
// Global initialization constant expression can only reference the imported globals.
// See the note on https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#constant-expressions%E2%91%A0
func executeConstExpression(importedGlobals []*GlobalInstance, expr *ConstantExpression) (v interface{}) {
// executeConstExpressionI32 executes the ConstantExpression which returns ValueTypeI32.
// The validity of the expression is ensured when calling this function as this is only called
// during instantiation phrase, and the validation happens in compilation (validateConstExpression).
func executeConstExpressionI32(importedGlobals []*GlobalInstance, expr *ConstantExpression) (ret int32) {
switch expr.Opcode {
case OpcodeI32Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
v, _, _ = leb128.LoadInt32(expr.Data)
case OpcodeI64Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
v, _, _ = leb128.LoadInt64(expr.Data)
case OpcodeF32Const:
v, _ = ieee754.DecodeFloat32(expr.Data)
case OpcodeF64Const:
v, _ = ieee754.DecodeFloat64(expr.Data)
ret, _, _ = leb128.LoadInt32(expr.Data)
case OpcodeGlobalGet:
id, _, _ := leb128.LoadUint32(expr.Data)
g := importedGlobals[id]
switch g.Type.ValType {
ret = int32(g.Val)
}
return
}
// initialize initializes the value of this global instance given the const expr and imported globals.
// funcRefResolver is called to get the actual funcref (engine specific) from the OpcodeRefFunc const expr.
//
// Global initialization constant expression can only reference the imported globals.
// See the note on https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#constant-expressions%E2%91%A0
func (g *GlobalInstance) initialize(importedGlobals []*GlobalInstance, expr *ConstantExpression, funcRefResolver func(funcIndex Index) Reference) {
switch expr.Opcode {
case OpcodeI32Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
v, _, _ := leb128.LoadInt32(expr.Data)
g.Val = uint64(uint32(v))
case OpcodeI64Const:
// Treat constants as signed as their interpretation is not yet known per /RATIONALE.md
v, _, _ := leb128.LoadInt64(expr.Data)
g.Val = uint64(v)
case OpcodeF32Const:
g.Val = uint64(binary.LittleEndian.Uint32(expr.Data))
case OpcodeF64Const:
g.Val = binary.LittleEndian.Uint64(expr.Data)
case OpcodeGlobalGet:
id, _, _ := leb128.LoadUint32(expr.Data)
importedG := importedGlobals[id]
switch importedG.Type.ValType {
case ValueTypeI32:
v = int32(g.Val)
g.Val = uint64(uint32(importedG.Val))
case ValueTypeI64:
v = int64(g.Val)
g.Val = importedG.Val
case ValueTypeF32:
v = api.DecodeF32(g.Val)
g.Val = importedG.Val
case ValueTypeF64:
v = api.DecodeF64(g.Val)
g.Val = importedG.Val
case ValueTypeV128:
v = [2]uint64{g.Val, g.ValHi}
g.Val, g.ValHi = importedG.Val, importedG.ValHi
case ValueTypeFuncref, ValueTypeExternref:
v = int64(g.Val)
g.Val = importedG.Val
}
case OpcodeRefNull:
switch expr.Data[0] {
case ValueTypeExternref, ValueTypeFuncref:
v = int64(0) // Reference types are opaque 64bit pointer at runtime.
g.Val = 0 // Reference types are opaque 64bit pointer at runtime.
}
case OpcodeRefFunc:
// For ref.func const expression, we temporarily store the index as value,
// and if this is the const expr for global, the value will be further downed to
// opaque pointer of the engine-specific compiled function.
v, _, _ = leb128.LoadUint32(expr.Data)
v, _, _ := leb128.LoadUint32(expr.Data)
g.Val = uint64(funcRefResolver(v))
case OpcodeVecV128Const:
v = [2]uint64{binary.LittleEndian.Uint64(expr.Data[0:8]), binary.LittleEndian.Uint64(expr.Data[8:16])}
g.Val, g.ValHi = binary.LittleEndian.Uint64(expr.Data[0:8]), binary.LittleEndian.Uint64(expr.Data[8:16])
}
return
}
func (s *Store) GetFunctionTypeIDs(ts []FunctionType) ([]FunctionTypeID, error) {

View File

@@ -501,10 +501,11 @@ func TestStore_getFunctionTypeID(t *testing.T) {
})
}
func TestExecuteConstExpression(t *testing.T) {
func TestGlobalInstance_initialize(t *testing.T) {
t.Run("basic type const expr", func(t *testing.T) {
for _, vt := range []ValueType{ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64} {
t.Run(ValueTypeName(vt), func(t *testing.T) {
g := &GlobalInstance{Type: GlobalType{ValType: vt}}
expr := &ConstantExpression{}
switch vt {
case ValueTypeI32:
@@ -521,35 +522,25 @@ func TestExecuteConstExpression(t *testing.T) {
expr.Opcode = OpcodeF64Const
}
raw := executeConstExpression(nil, expr)
require.NotNil(t, raw)
g.initialize(nil, expr, nil)
switch vt {
case ValueTypeI32:
actual, ok := raw.(int32)
require.True(t, ok)
require.Equal(t, int32(1), actual)
require.Equal(t, int32(1), int32(g.Val))
case ValueTypeI64:
actual, ok := raw.(int64)
require.True(t, ok)
require.Equal(t, int64(2), actual)
require.Equal(t, int64(2), int64(g.Val))
case ValueTypeF32:
actual, ok := raw.(float32)
require.True(t, ok)
require.Equal(t, float32(math.MaxFloat32), actual)
require.Equal(t, float32(math.MaxFloat32), math.Float32frombits(uint32(g.Val)))
case ValueTypeF64:
actual, ok := raw.(float64)
require.True(t, ok)
require.Equal(t, float64(math.MaxFloat64), actual)
require.Equal(t, math.MaxFloat64, math.Float64frombits(g.Val))
}
})
}
})
t.Run("reference types", func(t *testing.T) {
t.Run("ref.null", func(t *testing.T) {
tests := []struct {
name string
expr *ConstantExpression
exp interface{}
}{
{
name: "ref.null (externref)",
@@ -557,7 +548,6 @@ func TestExecuteConstExpression(t *testing.T) {
Opcode: OpcodeRefNull,
Data: []byte{RefTypeExternref},
},
exp: int64(0),
},
{
name: "ref.null (funcref)",
@@ -565,34 +555,30 @@ func TestExecuteConstExpression(t *testing.T) {
Opcode: OpcodeRefNull,
Data: []byte{RefTypeFuncref},
},
exp: int64(0),
},
{
name: "ref.func",
expr: &ConstantExpression{
Opcode: OpcodeRefFunc,
Data: []byte{1},
},
exp: uint32(1),
},
{
name: "ref.func",
expr: &ConstantExpression{
Opcode: OpcodeRefFunc,
Data: []byte{0x5d},
},
exp: uint32(93),
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
val := executeConstExpression(nil, tc.expr)
require.Equal(t, tc.exp, val)
g := GlobalInstance{}
g.Type.ValType = tc.expr.Data[0]
g.initialize(nil, tc.expr, nil)
require.Equal(t, uint64(0), g.Val)
})
}
})
t.Run("ref.func", func(t *testing.T) {
g := GlobalInstance{Type: GlobalType{ValType: RefTypeFuncref}}
g.initialize(nil,
&ConstantExpression{Opcode: OpcodeRefFunc, Data: []byte{1}},
func(funcIndex Index) Reference {
require.Equal(t, Index(1), funcIndex)
return 0xdeadbeaf
},
)
require.Equal(t, uint64(0xdeadbeaf), g.Val)
})
t.Run("global expr", func(t *testing.T) {
tests := []struct {
valueType ValueType
@@ -614,35 +600,23 @@ func TestExecuteConstExpression(t *testing.T) {
expr := &ConstantExpression{Data: []byte{0}, Opcode: OpcodeGlobalGet}
globals := []*GlobalInstance{{Val: tc.val, ValHi: tc.valHi, Type: GlobalType{ValType: tc.valueType}}}
val := executeConstExpression(globals, expr)
require.NotNil(t, val)
g := &GlobalInstance{Type: GlobalType{ValType: tc.valueType}}
g.initialize(globals, expr, nil)
switch tc.valueType {
case ValueTypeI32:
actual, ok := val.(int32)
require.True(t, ok)
require.Equal(t, int32(tc.val), actual)
require.Equal(t, int32(tc.val), int32(g.Val))
case ValueTypeI64:
actual, ok := val.(int64)
require.True(t, ok)
require.Equal(t, int64(tc.val), actual)
require.Equal(t, int64(tc.val), int64(g.Val))
case ValueTypeF32:
actual, ok := val.(float32)
require.True(t, ok)
require.Equal(t, api.DecodeF32(tc.val), actual)
require.Equal(t, tc.val, g.Val)
case ValueTypeF64:
actual, ok := val.(float64)
require.True(t, ok)
require.Equal(t, api.DecodeF64(tc.val), actual)
require.Equal(t, tc.val, g.Val)
case ValueTypeV128:
vector, ok := val.([2]uint64)
require.True(t, ok)
require.Equal(t, uint64(0x1), vector[0])
require.Equal(t, uint64(0x2), vector[1])
require.Equal(t, uint64(0x1), g.Val)
require.Equal(t, uint64(0x2), g.ValHi)
case ValueTypeFuncref, ValueTypeExternref:
actual, ok := val.(int64)
require.True(t, ok)
require.Equal(t, int64(tc.val), actual)
require.Equal(t, tc.val, g.Val)
}
})
}
@@ -653,12 +627,10 @@ func TestExecuteConstExpression(t *testing.T) {
1, 0, 0, 0, 0, 0, 0, 0,
2, 0, 0, 0, 0, 0, 0, 0,
}, Opcode: OpcodeVecV128Const}
val := executeConstExpression(nil, expr)
require.NotNil(t, val)
vector, ok := val.([2]uint64)
require.True(t, ok)
require.Equal(t, uint64(0x1), vector[0])
require.Equal(t, uint64(0x2), vector[1])
g := GlobalInstance{Type: GlobalType{ValType: ValueTypeV128}}
g.initialize(nil, expr, nil)
require.Equal(t, uint64(0x1), g.Val)
require.Equal(t, uint64(0x2), g.ValHi)
})
}