Repository: tetratelabs/wazero
Pull request #1257 (2023-03-20 03:06:24)

Avoides allocation with constExpr execution
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.

```
$ benchstat old.txt new.txt
name                                    old time/op    new time/op    delta
Initialization/interpreter-32             36.0µs ± 1%    36.1µs ± 3%    ~     (p=0.481 n=26+26)
Initialization/interpreter-multiple-32    38.5µs ± 9%    37.9µs ±10%    ~     (p=0.114 n=29+30)
Initialization/compiler-32                28.7µs ± 9%    27.9µs ±10%  -2.94%  (p=0.019 n=30+30)
Initialization/compiler-multiple-32       25.1µs ± 9%    23.9µs ± 6%  -4.69%  (p=0.000 n=30+29)
Compilation/with_extern_cache-32           197µs ± 1%     197µs ± 1%    ~     (p=0.971 n=30+30)
Compilation/without_extern_cache-32       5.89ms ± 1%    5.91ms ± 1%  +0.30%  (p=0.021 n=30+30)

name                                    old alloc/op   new alloc/op   delta
Initialization/interpreter-32              136kB ± 0%     136kB ± 0%  -0.01%  (p=0.000 n=24+30)
Initialization/interpreter-multiple-32     137kB ± 0%     137kB ± 0%  -0.01%  (p=0.000 n=27+27)
Initialization/compiler-32                 137kB ± 0%     137kB ± 0%  -0.01%  (p=0.000 n=26+23)
Initialization/compiler-multiple-32        142kB ± 0%     142kB ± 0%  -0.01%  (p=0.000 n=26+27)
Compilation/with_extern_cache-32          54.6kB ± 0%    54.6kB ± 0%  +0.01%  (p=0.022 n=30+30)
Compilation/without_extern_cache-32       1.99MB ± 0%    1.99MB ± 0%    ~     (p=0.416 n=26+30)

name                                    old allocs/op  new allocs/op  delta
Initialization/interpreter-32               38.0 ± 0%      35.0 ± 0%  -7.89%  (p=0.000 n=30+30)
Initialization/interpreter-multiple-32      57.0 ± 0%      54.0 ± 0%  -5.26%  (p=0.000 n=30+30)
Initialization/compiler-32                  38.0 ± 0%      35.0 ± 0%  -7.89%  (p=0.000 n=30+30)
Initialization/compiler-multiple-32         47.0 ± 0%      44.0 ± 0%  -6.38%  (p=0.000 n=30+30)
Compilation/with_extern_cache-32             982 ± 0%       982 ± 0%    ~     (all equal)
Compilation/without_extern_cache-32        32.6k ± 0%     32.6k ± 0%    ~     (p=0.913 n=30+30)
```

mathetake: > Compilation/without_extern_cache-32       5.89ms ± 1%    5.91ms ± 1%  +0.30%  (p=0.021 n=30+30)

this must be the noise - this commit doesn't change any code path of compilation.
mathetake: running fuzzing just in case.
mathetake: good to go

Patch:
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 1f7d7e70..ae1915ec 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -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
diff --git a/internal/wasm/store.go b/internal/wasm/store.go
index f7d61c19..e7fedb6a 100644
--- a/internal/wasm/store.go
+++ b/internal/wasm/store.go
@@ -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)
 }
 
+// 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:
+		ret, _, _ = leb128.LoadInt32(expr.Data)
+	case OpcodeGlobalGet:
+		id, _, _ := leb128.LoadUint32(expr.Data)
+		g := importedGlobals[id]
+		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 executeConstExpression(importedGlobals []*GlobalInstance, expr *ConstantExpression) (v interface{}) {
+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)
+		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)
+		v, _, _ := leb128.LoadInt64(expr.Data)
+		g.Val = uint64(v)
 	case OpcodeF32Const:
-		v, _ = ieee754.DecodeFloat32(expr.Data)
+		g.Val = uint64(binary.LittleEndian.Uint32(expr.Data))
 	case OpcodeF64Const:
-		v, _ = ieee754.DecodeFloat64(expr.Data)
+		g.Val = binary.LittleEndian.Uint64(expr.Data)
 	case OpcodeGlobalGet:
 		id, _, _ := leb128.LoadUint32(expr.Data)
-		g := importedGlobals[id]
-		switch g.Type.ValType {
+		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) {


Test patch:
diff --git a/internal/wasm/store_test.go b/internal/wasm/store_test.go
index fbb41a9a..b2dfcc6d 100644
--- a/internal/wasm/store_test.go
+++ b/internal/wasm/store_test.go
@@ -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)
 	})
 }