Repository: tetratelabs/wazero
Pull request #2041 (2024-02-11 18:13:18)

wazevo(ssa): reuse slices
cut huge alloc counts: 

```
pkg: github.com/tetratelabs/wazero/internal/integration_test/stdlibs
                                        │   old.txt   │            new.txt             │
                                        │   sec/op    │   sec/op     vs base           │
Zig/optimizing/Compile/test-opt.wasm-10   5.136 ± ∞ ¹   5.130 ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

                                        │    old.txt    │             new.txt              │
                                        │     B/op      │     B/op       vs base           │
Zig/optimizing/Compile/test-opt.wasm-10   487.4Mi ± ∞ ¹   472.1Mi ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05

                                        │   old.txt    │             new.txt             │
                                        │  allocs/op   │  allocs/op    vs base           │
Zig/optimizing/Compile/test-opt.wasm-10   793.1k ± ∞ ¹   674.3k ± ∞ ¹  ~ (p=1.000 n=1) ²
¹ need >= 6 samples for confidence interval at level 0.95
² need >= 4 samples to detect a difference at alpha level 0.05
```


Patch:
diff --git a/internal/engine/wazevo/frontend/frontend.go b/internal/engine/wazevo/frontend/frontend.go
index 3c8140aa..2ec53406 100644
--- a/internal/engine/wazevo/frontend/frontend.go
+++ b/internal/engine/wazevo/frontend/frontend.go
@@ -28,7 +28,6 @@ type Compiler struct {
 	tableGrowSig           ssa.Signature
 	refFuncSig             ssa.Signature
 	memmoveSig             ssa.Signature
-	checkModuleExitCodeArg [1]ssa.Value
 	ensureTermination      bool
 
 	// Followings are reset by per function.
@@ -450,3 +449,13 @@ func (c *Compiler) clearSafeBounds() {
 func (k *knownSafeBound) valid() bool {
 	return k != nil && k.bound > 0
 }
+
+func (c *Compiler) allocateVarLengthValues(vs ...ssa.Value) ssa.Values {
+	builder := c.ssaBuilder
+	pool := builder.VarLengthPool()
+	args := pool.Allocate(len(vs))
+	for _, v := range vs {
+		args = args.Append(builder.VarLengthPool(), v)
+	}
+	return args
+}
diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go
index 2d18a9fb..02a75ad7 100644
--- a/internal/engine/wazevo/frontend/lower.go
+++ b/internal/engine/wazevo/frontend/lower.go
@@ -35,7 +35,7 @@ type (
 		followingBlock ssa.BasicBlock
 		blockType *wasm.FunctionType
 		// clonedArgs hold the arguments to Else block.
-		clonedArgs []ssa.Value
+		clonedArgs ssa.Values
 	}
 
 	controlFrameKind byte
@@ -114,24 +114,18 @@ func (l *loweringState) push(ret ssa.Value) {
 	l.values = append(l.values, ret)
 }
 
-func (l *loweringState) nPopInto(n int, dst []ssa.Value) {
+func (c *Compiler) nPeekDup(n int) ssa.Values {
 	if n == 0 {
-		return
+		return ssa.ValuesNil
 	}
-	tail := len(l.values)
-	begin := tail - n
-	view := l.values[begin:tail]
-	copy(dst, view)
-	l.values = l.values[:begin]
-}
+	args := c.allocateVarLengthValues()
 
-func (l *loweringState) nPeekDup(n int) []ssa.Value {
-	if n == 0 {
-		return nil
-	}
+	l := c.state()
 	tail := len(l.values)
-	view := l.values[tail-n : tail]
-	return cloneValuesList(view)
+	for _, v := range l.values[tail-n : tail] {
+		args = args.Append(c.ssaBuilder.VarLengthPool(), v)
+	}
+	return args
 }
 
 func (l *loweringState) ctrlPop() (ret controlFrame) {
@@ -577,8 +571,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 					ssa.TypeI64,
 				).Insert(builder).Return()
 
-			// TODO: reuse the slice.
-			args := []ssa.Value{c.execCtxPtrValue, tableIndexVal, num, r}
+			args := c.allocateVarLengthValues(c.execCtxPtrValue, tableIndexVal, num, r)
 			callGrowRet := builder.
 				AllocateInstruction().
 				AsCallIndirect(tableGrowPtr, &c.tableGrowSig, args).
@@ -680,14 +673,14 @@ func (c *Compiler) lowerCurrentOpcode() {
 			zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder).Return()
 			ifFillSizeZero := builder.AllocateInstruction().AsIcmp(fillSizeExt, zero, ssa.IntegerCmpCondEqual).
 				Insert(builder).Return()
-			builder.AllocateInstruction().AsBrnz(ifFillSizeZero, nil, followingBlk).Insert(builder)
-			c.insertJumpToBlock(nil, beforeLoop)
+			builder.AllocateInstruction().AsBrnz(ifFillSizeZero, ssa.ValuesNil, followingBlk).Insert(builder)
+			c.insertJumpToBlock(ssa.ValuesNil, beforeLoop)
 
 			// buf[0:8] = value
 			builder.SetCurrentBlock(beforeLoop)
 			builder.AllocateInstruction().AsStore(ssa.OpcodeStore, value, addr, 0).Insert(builder)
 			initValue := builder.AllocateInstruction().AsIconst64(8).Insert(builder).Return()
-			c.insertJumpToBlock([]ssa.Value{initValue}, loopBlk) // TODO: reuse the slice.
+			c.insertJumpToBlock(c.allocateVarLengthValues(initValue), loopBlk)
 
 			builder.SetCurrentBlock(loopBlk)
 			dstAddr := builder.AllocateInstruction().AsIadd(addr, loopVar).Insert(builder).Return()
@@ -711,10 +704,10 @@ func (c *Compiler) lowerCurrentOpcode() {
 				AsIcmp(newLoopVar, fillSizeInBytes, ssa.IntegerCmpCondUnsignedLessThan).Insert(builder).Return()
 
 			builder.AllocateInstruction().
-				AsBrnz(loopVarLessThanFillSize, []ssa.Value{newLoopVar}, loopBlk). // TODO: reuse the slice.
+				AsBrnz(loopVarLessThanFillSize, c.allocateVarLengthValues(newLoopVar), loopBlk).
 				Insert(builder)
 
-			c.insertJumpToBlock(nil, followingBlk)
+			c.insertJumpToBlock(ssa.ValuesNil, followingBlk)
 			builder.SetCurrentBlock(followingBlk)
 
 			builder.Seal(beforeLoop)
@@ -756,14 +749,14 @@ func (c *Compiler) lowerCurrentOpcode() {
 			zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder).Return()
 			ifFillSizeZero := builder.AllocateInstruction().AsIcmp(fillSize, zero, ssa.IntegerCmpCondEqual).
 				Insert(builder).Return()
-			builder.AllocateInstruction().AsBrnz(ifFillSizeZero, nil, followingBlk).Insert(builder)
-			c.insertJumpToBlock(nil, beforeLoop)
+			builder.AllocateInstruction().AsBrnz(ifFillSizeZero, ssa.ValuesNil, followingBlk).Insert(builder)
+			c.insertJumpToBlock(ssa.ValuesNil, beforeLoop)
 
 			// buf[0] = value
 			builder.SetCurrentBlock(beforeLoop)
 			builder.AllocateInstruction().AsStore(ssa.OpcodeIstore8, value, addr, 0).Insert(builder)
 			initValue := builder.AllocateInstruction().AsIconst64(1).Insert(builder).Return()
-			c.insertJumpToBlock([]ssa.Value{initValue}, loopBlk) // TODO: reuse the slice.
+			c.insertJumpToBlock(c.allocateVarLengthValues(initValue), loopBlk)
 
 			builder.SetCurrentBlock(loopBlk)
 			dstAddr := builder.AllocateInstruction().AsIadd(addr, loopVar).Insert(builder).Return()
@@ -787,10 +780,10 @@ func (c *Compiler) lowerCurrentOpcode() {
 				AsIcmp(newLoopVar, fillSize, ssa.IntegerCmpCondUnsignedLessThan).Insert(builder).Return()
 
 			builder.AllocateInstruction().
-				AsBrnz(loopVarLessThanFillSize, []ssa.Value{newLoopVar}, loopBlk). // TODO: reuse the slice.
+				AsBrnz(loopVarLessThanFillSize, c.allocateVarLengthValues(newLoopVar), loopBlk).
 				Insert(builder)
 
-			c.insertJumpToBlock(nil, followingBlk)
+			c.insertJumpToBlock(ssa.ValuesNil, followingBlk)
 			builder.SetCurrentBlock(followingBlk)
 
 			builder.Seal(beforeLoop)
@@ -1171,9 +1164,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 				ssa.TypeI64,
 			).Insert(builder).Return()
 
-		// TODO: reuse the slice.
-		args := []ssa.Value{c.execCtxPtrValue, pages}
-
+		args := c.allocateVarLengthValues(c.execCtxPtrValue, pages)
 		callGrowRet := builder.
 			AllocateInstruction().
 			AsCallIndirect(memoryGrowPtr, &c.memoryGrowSig, args).
@@ -1343,9 +1334,9 @@ func (c *Compiler) lowerCurrentOpcode() {
 			blockType:                    bt,
 		})
 
-		var args []ssa.Value
-		if len(bt.Params) > 0 {
-			args = cloneValuesList(state.values[originalLen:])
+		args := c.allocateVarLengthValues()
+		for _, v := range state.values[originalLen:] {
+			args = args.Append(builder.VarLengthPool(), v)
 		}
 
 		// Insert the jump to the header of loop.
@@ -1362,10 +1353,9 @@ func (c *Compiler) lowerCurrentOpcode() {
 					ssa.TypeI64,
 				).Insert(builder).Return()
 
-			c.checkModuleExitCodeArg[0] = c.execCtxPtrValue
-
+			args := c.allocateVarLengthValues(c.execCtxPtrValue)
 			builder.AllocateInstruction().
-				AsCallIndirect(checkModuleExitCodePtr, &c.checkModuleExitCodeSig, c.checkModuleExitCodeArg[:]).
+				AsCallIndirect(checkModuleExitCodePtr, &c.checkModuleExitCodeSig, args).
 				Insert(builder)
 		}
 	case wasm.OpcodeIf:
@@ -1386,19 +1376,19 @@ func (c *Compiler) lowerCurrentOpcode() {
 		// multiple definitions (one in Then and another in Else blocks).
 		c.addBlockParamsFromWasmTypes(bt.Results, followingBlk)
 
-		var args []ssa.Value
-		if len(bt.Params) > 0 {
-			args = cloneValuesList(state.values[len(state.values)-len(bt.Params):])
+		args := c.allocateVarLengthValues()
+		for _, v := range state.values[len(state.values)-len(bt.Params):] {
+			args = args.Append(builder.VarLengthPool(), v)
 		}
 
 		// Insert the conditional jump to the Else block.
 		brz := builder.AllocateInstruction()
-		brz.AsBrz(v, nil, elseBlk)
+		brz.AsBrz(v, ssa.ValuesNil, elseBlk)
 		builder.InsertInstruction(brz)
 
 		// Then, insert the jump to the Then block.
 		br := builder.AllocateInstruction()
-		br.AsJump(nil, thenBlk)
+		br.AsJump(ssa.ValuesNil, thenBlk)
 		builder.InsertInstruction(br)
 
 		state.ctrlPush(controlFrame{
@@ -1429,7 +1419,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 		if !state.unreachable {
 			// If this Then block is currently reachable, we have to insert the branching to the following BB.
 			followingBlk := ifctrl.followingBlock // == the BB after if-then-else.
-			args := c.loweringState.nPeekDup(len(ifctrl.blockType.Results))
+			args := c.nPeekDup(len(ifctrl.blockType.Results))
 			c.insertJumpToBlock(args, followingBlk)
 		} else {
 			state.unreachable = false
@@ -1438,7 +1428,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 		// Reset the stack so that we can correctly handle the else block.
 		state.values = state.values[:ifctrl.originalStackLenWithoutParam]
 		elseBlk := ifctrl.blk
-		for _, arg := range ifctrl.clonedArgs {
+		for _, arg := range ifctrl.clonedArgs.View() {
 			state.push(arg)
 		}
 
@@ -1458,7 +1448,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 		unreachable := state.unreachable
 		if !unreachable {
 			// Top n-th args will be used as a result of the current control frame.
-			args := c.loweringState.nPeekDup(len(ctrl.blockType.Results))
+			args := c.nPeekDup(len(ctrl.blockType.Results))
 
 			// Insert the unconditional branch to the target.
 			c.insertJumpToBlock(args, followingBlk)
@@ -1492,7 +1482,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 		}
 
 		targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
-		args := c.loweringState.nPeekDup(argNum)
+		args := c.nPeekDup(argNum)
 		c.insertJumpToBlock(args, targetBlk)
 
 		state.unreachable = true
@@ -1506,7 +1496,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 		v := state.pop()
 
 		targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
-		args := c.loweringState.nPeekDup(argNum)
+		args := c.nPeekDup(argNum)
 
 		// Insert the conditional jump to the target block.
 		brnz := builder.AllocateInstruction()
@@ -1515,7 +1505,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 
 		// Insert the unconditional jump to the Else block which corresponds to after br_if.
 		elseBlk := builder.AllocateBasicBlock()
-		c.insertJumpToBlock(nil, elseBlk)
+		c.insertJumpToBlock(ssa.ValuesNil, elseBlk)
 
 		// Now start translating the instructions after br_if.
 		builder.Seal(elseBlk) // Else of br_if has the current block as the only one successor.
@@ -1536,7 +1526,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 		index := state.pop()
 		if labelCount == 0 { // If this br_table is empty, we can just emit the unconditional jump.
 			targetBlk, argNum := state.brTargetArgNumFor(labels[0])
-			args := c.loweringState.nPeekDup(argNum)
+			args := c.nPeekDup(argNum)
 			c.insertJumpToBlock(args, targetBlk)
 		} else {
 			c.lowerBrTable(labels, index)
@@ -1552,7 +1542,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 			c.callListenerAfter()
 		}
 
-		results := c.loweringState.nPeekDup(c.results())
+		results := c.nPeekDup(c.results())
 		instr := builder.AllocateInstruction()
 
 		instr.AsReturn(results)
@@ -1606,16 +1596,19 @@ func (c *Compiler) lowerCurrentOpcode() {
 		}
 		typ := &c.m.TypeSection[typIndex]
 
-		// TODO: reuse slice?
 		argN := len(typ.Params)
-		args := make([]ssa.Value, argN+2)
-		args[0] = c.execCtxPtrValue
-		state.nPopInto(argN, args[2:])
+		tail := len(state.values) - argN
+		vs := state.values[tail:]
+		state.values = state.values[:tail]
+		args := c.allocateVarLengthValues(c.execCtxPtrValue)
 
 		sig := c.signatures[typ]
 		call := builder.AllocateInstruction()
 		if fnIndex >= c.m.ImportFunctionCount {
-			args[1] = c.moduleCtxPtrValue // This case the callee module is itself.
+			args = args.Append(builder.VarLengthPool(), c.moduleCtxPtrValue) // This case the callee module is itself.
+			for _, v := range vs {
+				args = args.Append(builder.VarLengthPool(), v)
+			}
 			call.AsCall(FunctionIndexToFuncRef(fnIndex), sig, args)
 			builder.InsertInstruction(call)
 		} else {
@@ -1628,8 +1621,10 @@ func (c *Compiler) lowerCurrentOpcode() {
 			builder.InsertInstruction(loadFuncPtr)
 			builder.InsertInstruction(loadModuleCtxPtr)
 
-			args[1] = loadModuleCtxPtr.Return() // This case the callee module is itself.
-
+			args = args.Append(builder.VarLengthPool(), loadModuleCtxPtr.Return()) // This case the callee module is itself.
+			for _, v := range vs {
+				args = args.Append(builder.VarLengthPool(), v)
+			}
 			call.AsCallIndirect(loadFuncPtr.Return(), sig, args)
 			builder.InsertInstruction(call)
 		}
@@ -3189,7 +3184,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 					ssa.TypeI64,
 				).Insert(builder).Return()
 
-			args := []ssa.Value{c.execCtxPtrValue, timeout, exp, addr}
+			args := c.allocateVarLengthValues(c.execCtxPtrValue, timeout, exp, addr)
 			memoryWaitRet := builder.AllocateInstruction().
 				AsCallIndirect(memoryWaitPtr, sig, args).
 				Insert(builder).Return()
@@ -3211,7 +3206,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 					wazevoapi.ExecutionContextOffsetMemoryNotifyTrampolineAddress.U32(),
 					ssa.TypeI64,
 				).Insert(builder).Return()
-			args := []ssa.Value{c.execCtxPtrValue, count, addr}
+			args := c.allocateVarLengthValues(c.execCtxPtrValue, count, addr)
 			memoryNotifyRet := builder.AllocateInstruction().
 				AsCallIndirect(memoryNotifyPtr, &c.memoryNotifySig, args).
 				Insert(builder).Return()
@@ -3421,8 +3416,7 @@ func (c *Compiler) lowerCurrentOpcode() {
 				ssa.TypeI64,
 			).Insert(builder).Return()
 
-		// TODO: reuse the slice.
-		args := []ssa.Value{c.execCtxPtrValue, funcIndexVal}
+		args := c.allocateVarLengthValues(c.execCtxPtrValue, funcIndexVal)
 		refFuncRet := builder.
 			AllocateInstruction().
 			AsCallIndirect(refFuncPtr, &c.refFuncSig, args).
@@ -3597,13 +3591,14 @@ func (c *Compiler) lowerCallIndirect(typeIndex, tableIndex uint32) {
 	builder.InsertInstruction(loadModuleContextOpaquePtr)
 	moduleContextOpaquePtr := loadModuleContextOpaquePtr.Return()
 
-	// TODO: reuse slice?
 	typ := &c.m.TypeSection[typeIndex]
-	argN := len(typ.Params)
-	args := make([]ssa.Value, argN+2)
-	args[0] = c.execCtxPtrValue
-	args[1] = moduleContextOpaquePtr
-	state.nPopInto(argN, args[2:])
+	tail := len(state.values) - len(typ.Params)
+	vs := state.values[tail:]
+	state.values = state.values[:tail]
+	args := c.allocateVarLengthValues(c.execCtxPtrValue, moduleContextOpaquePtr)
+	for _, v := range vs {
+		args = args.Append(builder.VarLengthPool(), v)
+	}
 
 	// Before transfer the control to the callee, we have to store the current module's moduleContextPtr
 	// into execContext.callerModuleContextPtr in case when the callee is a Go function.
@@ -3701,7 +3696,7 @@ func (c *Compiler) memAlignmentCheck(addr ssa.Value, operationSizeInBytes uint64
 }
 
 func (c *Compiler) callMemmove(dst, src, size ssa.Value) {
-	args := []ssa.Value{dst, src, size} // TODO: reuse the slice.
+	args := c.allocateVarLengthValues(dst, src, size)
 	if size.Type() != ssa.TypeI64 {
 		panic("TODO: memmove size must be i64")
 	}
@@ -3969,7 +3964,7 @@ func (c *Compiler) readMemArg() (align, offset uint32) {
 }
 
 // insertJumpToBlock inserts a jump instruction to the given block in the current block.
-func (c *Compiler) insertJumpToBlock(args []ssa.Value, targetBlk ssa.BasicBlock) {
+func (c *Compiler) insertJumpToBlock(args ssa.Values, targetBlk ssa.BasicBlock) {
 	if targetBlk.ReturnBlock() {
 		if c.needListener {
 			c.callListenerAfter()
@@ -4015,15 +4010,6 @@ func (c *Compiler) switchTo(originalStackLen int, targetBlk ssa.BasicBlock) {
 	}
 }
 
-// cloneValuesList clones the given values list.
-func cloneValuesList(in []ssa.Value) (ret []ssa.Value) {
-	ret = make([]ssa.Value, len(in))
-	for i := range ret {
-		ret[i] = in[i]
-	}
-	return
-}
-
 // results returns the number of results of the current function.
 func (c *Compiler) results() int {
 	return len(c.wasmFunctionTyp.Results)
@@ -4050,7 +4036,7 @@ func (c *Compiler) lowerBrTable(labels []uint32, index ssa.Value) {
 	for i, l := range labels {
 		// Args are always on the top of the stack. Note that we should not share the args slice
 		// among the jump instructions since the args are modified during passes (e.g. redundant phi elimination).
-		args := c.loweringState.nPeekDup(numArgs)
+		args := c.nPeekDup(numArgs)
 		targetBlk, _ := state.brTargetArgNumFor(l)
 		trampoline := builder.AllocateBasicBlock()
 		builder.SetCurrentBlock(trampoline)
@@ -4094,12 +4080,11 @@ func (c *Compiler) callListenerBefore() {
 
 	entry := builder.EntryBlock()
 	ps := entry.Params()
-	// TODO: reuse!
-	args := make([]ssa.Value, ps)
-	args[0] = c.execCtxPtrValue
-	args[1] = builder.AllocateInstruction().AsIconst32(c.wasmLocalFunctionIndex).Insert(builder).Return()
+
+	args := c.allocateVarLengthValues(c.execCtxPtrValue,
+		builder.AllocateInstruction().AsIconst32(c.wasmLocalFunctionIndex).Insert(builder).Return())
 	for i := 2; i < ps; i++ {
-		args[i] = entry.Param(i)
+		args = args.Append(builder.VarLengthPool(), entry.Param(i))
 	}
 
 	beforeSig := c.listenerSignatures[c.wasmFunctionTyp][0]
@@ -4125,14 +4110,13 @@ func (c *Compiler) callListenerAfter() {
 		Return()
 
 	afterSig := c.listenerSignatures[c.wasmFunctionTyp][1]
-	results := c.loweringState.nPeekDup(c.results())
-
-	// TODO: reuse!
-	args := make([]ssa.Value, len(results)+2)
-	args[0] = c.execCtxPtrValue
-	args[1] = builder.AllocateInstruction().AsIconst32(c.wasmLocalFunctionIndex).Insert(builder).Return()
-	for i, r := range results {
-		args[i+2] = r
+	args := c.allocateVarLengthValues(c.execCtxPtrValue,
+		builder.AllocateInstruction().AsIconst32(c.wasmLocalFunctionIndex).Insert(builder).Return())
+
+	l := c.state()
+	tail := len(l.values)
+	for _, v := range l.values[tail-c.results() : tail] {
+		args = args.Append(c.ssaBuilder.VarLengthPool(), v)
 	}
 	builder.AllocateInstruction().
 		AsCallIndirect(afterListenerPtr, afterSig, args).
diff --git a/internal/engine/wazevo/ssa/basic_block.go b/internal/engine/wazevo/ssa/basic_block.go
index 1dbeaf1e..49e4c0f8 100644
--- a/internal/engine/wazevo/ssa/basic_block.go
+++ b/internal/engine/wazevo/ssa/basic_block.go
@@ -371,11 +371,11 @@ func (bb *basicBlock) validate(b *builder) {
 				exp = len(bb.params)
 			}
 
-			if len(pred.branch.vs) != exp {
+			if len(pred.branch.vs.View()) != exp {
 				panic(fmt.Sprintf(
 					"BUG: len(argument at %s) != len(params at %s): %d != %d: %s",
 					pred.blk.Name(), bb.Name(),
-					len(pred.branch.vs), len(bb.params), pred.branch.Format(b),
+					len(pred.branch.vs.View()), len(bb.params), pred.branch.Format(b),
 				))
 			}
 
diff --git a/internal/engine/wazevo/ssa/builder.go b/internal/engine/wazevo/ssa/builder.go
index 7351bad6..b89c9e7e 100644
--- a/internal/engine/wazevo/ssa/builder.go
+++ b/internal/engine/wazevo/ssa/builder.go
@@ -130,6 +130,8 @@ type Builder interface {
 
 	// Idom returns the immediate dominator of the given BasicBlock.
 	Idom(blk BasicBlock) BasicBlock
+
+	VarLengthPool() *wazevoapi.VarLengthPool[Value]
 }
 
 // NewBuilder returns a new Builder implementation.
@@ -137,6 +139,7 @@ func NewBuilder() Builder {
 	return &builder{
 		instructionsPool:               wazevoapi.NewPool[Instruction](resetInstruction),
 		basicBlocksPool:                wazevoapi.NewPool[basicBlock](resetBasicBlock),
+		varLengthPool:                  wazevoapi.NewVarLengthPool[Value](),
 		valueAnnotations:               make(map[ValueID]string),
 		signatures:                     make(map[SignatureID]*Signature),
 		blkVisited:                     make(map[*basicBlock]int),
@@ -150,6 +153,7 @@ func NewBuilder() Builder {
 type builder struct {
 	basicBlocksPool  wazevoapi.Pool[basicBlock]
 	instructionsPool wazevoapi.Pool[Instruction]
+	varLengthPool    wazevoapi.VarLengthPool[Value]
 	signatures       map[SignatureID]*Signature
 	currentSignature *Signature
 
@@ -200,6 +204,10 @@ type builder struct {
 	currentSourceOffset SourceOffset
 }
 
+func (b *builder) VarLengthPool() *wazevoapi.VarLengthPool[Value] {
+	return &b.varLengthPool
+}
+
 // ReturnBlock implements Builder.ReturnBlock.
 func (b *builder) ReturnBlock() BasicBlock {
 	return b.returnBlk
@@ -212,6 +220,7 @@ func (b *builder) Init(s *Signature) {
 	resetBasicBlock(b.returnBlk)
 	b.instructionsPool.Reset()
 	b.basicBlocksPool.Reset()
+	b.varLengthPool.Reset()
 	b.donePasses = false
 	for _, sig := range b.signatures {
 		sig.used = false
@@ -351,11 +360,11 @@ func (b *builder) InsertInstruction(instr *Instruction) {
 		return
 	}
 
-	// TODO: reuse slices, though this seems not to be common.
-	instr.rValues = make([]Value, tsl)
+	rValues := b.varLengthPool.Allocate(tsl)
 	for i := 0; i < tsl; i++ {
-		instr.rValues[i] = b.allocateValue(ts[i])
+		rValues = rValues.Append(&b.varLengthPool, b.allocateValue(ts[i]))
 	}
+	instr.rValues = rValues
 }
 
 // DefineVariable implements Builder.DefineVariable.
@@ -494,7 +503,7 @@ func (b *builder) findValue(typ Type, variable Variable, blk *basicBlock) Value
 	for i := range blk.preds {
 		pred := &blk.preds[i]
 		value := b.findValue(typ, variable, pred.blk)
-		pred.branch.addArgumentBranchInst(value)
+		pred.branch.addArgumentBranchInst(b, value)
 	}
 	return paramValue
 }
@@ -517,7 +526,7 @@ func (b *builder) Seal(raw BasicBlock) {
 			if !predValue.Valid() {
 				panic("BUG: value is not defined anywhere in the predecessors in the CFG")
 			}
-			pred.branch.addArgumentBranchInst(predValue)
+			pred.branch.addArgumentBranchInst(b, predValue)
 		}
 	}
 }
@@ -657,8 +666,9 @@ func (b *builder) resolveArgumentAlias(instr *Instruction) {
 		instr.v3 = b.resolveAlias(instr.v3)
 	}
 
-	for i, v := range instr.vs {
-		instr.vs[i] = b.resolveAlias(v)
+	view := instr.vs.View()
+	for i, v := range view {
+		view[i] = b.resolveAlias(v)
 	}
 }
 
@@ -885,7 +895,7 @@ func maybeInvertBranches(now *basicBlock, nextInRPO *basicBlock) bool {
 		return false
 	}
 
-	if len(fallthroughBranch.vs) != 0 || len(condBranch.vs) != 0 {
+	if len(fallthroughBranch.vs.View()) != 0 || len(condBranch.vs.View()) != 0 {
 		// If either one of them has arguments, we don't invert the branches.
 		return false
 	}
diff --git a/internal/engine/wazevo/ssa/instructions.go b/internal/engine/wazevo/ssa/instructions.go
index 26b37c80..093dc3e2 100644
--- a/internal/engine/wazevo/ssa/instructions.go
+++ b/internal/engine/wazevo/ssa/instructions.go
@@ -23,14 +23,14 @@ type Instruction struct {
 	v          Value
 	v2         Value
 	v3         Value
-	vs         []Value
+	vs         Values
 	typ        Type
 	blk        BasicBlock
 	targets    []BasicBlock
 	prev, next *Instruction
 
 	rValue         Value
-	rValues        []Value
+	rValues        Values
 	gid            InstructionGroupID
 	sourceOffset   SourceOffset
 	live           bool
@@ -84,7 +84,7 @@ func resetInstruction(i *Instruction) {
 	i.v3 = ValueInvalid
 	i.rValue = ValueInvalid
 	i.typ = typeInvalid
-	i.vs = nil
+	i.vs = ValuesNil
 	i.sourceOffset = sourceOffsetUnknown
 }
 
@@ -105,7 +105,7 @@ type InstructionGroupID uint32
 // Returns Value(s) produced by this instruction if any.
 // The `first` is the first return value, and `rest` is the rest of the values.
 func (i *Instruction) Returns() (first Value, rest []Value) {
-	return i.rValue, i.rValues
+	return i.rValue, i.rValues.View()
 }
 
 // Return returns a Value(s) produced by this instruction if any.
@@ -116,7 +116,7 @@ func (i *Instruction) Return() (first Value) {
 
 // Args returns the arguments to this instruction.
 func (i *Instruction) Args() (v1, v2, v3 Value, vs []Value) {
-	return i.v, i.v2, i.v3, i.vs
+	return i.v, i.v2, i.v3, i.vs.View()
 }
 
 // Arg returns the first argument to this instruction.
@@ -1935,7 +1935,7 @@ func (i *Instruction) VconstData() (lo, hi uint64) {
 }
 
 // AsReturn initializes this instruction as a return instruction with OpcodeReturn.
-func (i *Instruction) AsReturn(vs []Value) *Instruction {
+func (i *Instruction) AsReturn(vs wazevoapi.VarLength[Value]) *Instruction {
 	i.opcode = OpcodeReturn
 	i.vs = vs
 	return i
@@ -2033,7 +2033,7 @@ func (i *Instruction) AtomicCasData() (size uint64) {
 
 // ReturnVals returns the return values of OpcodeReturn.
 func (i *Instruction) ReturnVals() []Value {
-	return i.vs
+	return i.vs.View()
 }
 
 // AsExitWithCode initializes this instruction as a trap instruction with OpcodeExitWithCode.
@@ -2084,7 +2084,7 @@ func (i *Instruction) BranchData() (condVal Value, blockArgs []Value, target Bas
 	default:
 		panic("BUG")
 	}
-	blockArgs = i.vs
+	blockArgs = i.vs.View()
 	target = i.blk
 	return
 }
@@ -2100,7 +2100,7 @@ func (i *Instruction) BrTableData() (index Value, targets []BasicBlock) {
 }
 
 // AsJump initializes this instruction as a jump instruction with OpcodeJump.
-func (i *Instruction) AsJump(vs []Value, target BasicBlock) {
+func (i *Instruction) AsJump(vs Values, target BasicBlock) {
 	i.opcode = OpcodeJump
 	i.vs = vs
 	i.blk = target
@@ -2123,7 +2123,7 @@ func (i *Instruction) AsFallthroughJump() {
 }
 
 // AsBrz initializes this instruction as a branch-if-zero instruction with OpcodeBrz.
-func (i *Instruction) AsBrz(v Value, args []Value, target BasicBlock) {
+func (i *Instruction) AsBrz(v Value, args Values, target BasicBlock) {
 	i.opcode = OpcodeBrz
 	i.v = v
 	i.vs = args
@@ -2131,7 +2131,7 @@ func (i *Instruction) AsBrz(v Value, args []Value, target BasicBlock) {
 }
 
 // AsBrnz initializes this instruction as a branch-if-not-zero instruction with OpcodeBrnz.
-func (i *Instruction) AsBrnz(v Value, args []Value, target BasicBlock) *Instruction {
+func (i *Instruction) AsBrnz(v Value, args Values, target BasicBlock) *Instruction {
 	i.opcode = OpcodeBrnz
 	i.v = v
 	i.vs = args
@@ -2147,7 +2147,7 @@ func (i *Instruction) AsBrTable(index Value, targets []BasicBlock) {
 }
 
 // AsCall initializes this instruction as a call instruction with OpcodeCall.
-func (i *Instruction) AsCall(ref FuncRef, sig *Signature, args []Value) {
+func (i *Instruction) AsCall(ref FuncRef, sig *Signature, args Values) {
 	i.opcode = OpcodeCall
 	i.u1 = uint64(ref)
 	i.vs = args
@@ -2162,12 +2162,12 @@ func (i *Instruction) CallData() (ref FuncRef, sigID SignatureID, args []Value)
 	}
 	ref = FuncRef(i.u1)
 	sigID = SignatureID(i.u2)
-	args = i.vs
+	args = i.vs.View()
 	return
 }
 
 // AsCallIndirect initializes this instruction as a call-indirect instruction with OpcodeCallIndirect.
-func (i *Instruction) AsCallIndirect(funcPtr Value, sig *Signature, args []Value) *Instruction {
+func (i *Instruction) AsCallIndirect(funcPtr Value, sig *Signature, args Values) *Instruction {
 	i.opcode = OpcodeCallIndirect
 	i.typ = TypeF64
 	i.vs = args
@@ -2184,7 +2184,7 @@ func (i *Instruction) CallIndirectData() (funcPtr Value, sigID SignatureID, args
 	}
 	funcPtr = i.v
 	sigID = SignatureID(i.u1)
-	args = i.vs
+	args = i.vs.View()
 	return
 }
 
@@ -2471,9 +2471,10 @@ func (i *Instruction) Format(b Builder) string {
 	case OpcodeSExtend, OpcodeUExtend:
 		instSuffix = fmt.Sprintf(" %s, %d->%d", i.v.Format(b), i.u1>>8, i.u1&0xff)
 	case OpcodeCall, OpcodeCallIndirect:
-		vs := make([]string, len(i.vs))
+		view := i.vs.View()
+		vs := make([]string, len(view))
 		for idx := range vs {
-			vs[idx] = i.vs[idx].Format(b)
+			vs[idx] = view[idx].Format(b)
 		}
 		if i.opcode == OpcodeCallIndirect {
 			instSuffix = fmt.Sprintf(" %s:%s, %s", i.v.Format(b), SignatureID(i.u1), strings.Join(vs, ", "))
@@ -2504,32 +2505,35 @@ func (i *Instruction) Format(b Builder) string {
 	case OpcodeF64const:
 		instSuffix = fmt.Sprintf(" %f", math.Float64frombits(i.u1))
 	case OpcodeReturn:
-		if len(i.vs) == 0 {
+		view := i.vs.View()
+		if len(view) == 0 {
 			break
 		}
-		vs := make([]string, len(i.vs))
+		vs := make([]string, len(view))
 		for idx := range vs {
-			vs[idx] = i.vs[idx].Format(b)
+			vs[idx] = view[idx].Format(b)
 		}
 		instSuffix = fmt.Sprintf(" %s", strings.Join(vs, ", "))
 	case OpcodeJump:
-		vs := make([]string, len(i.vs)+1)
+		view := i.vs.View()
+		vs := make([]string, len(view)+1)
 		if i.IsFallthroughJump() {
 			vs[0] = " fallthrough"
 		} else {
 			vs[0] = " " + i.blk.(*basicBlock).Name()
 		}
-		for idx := range i.vs {
-			vs[idx+1] = i.vs[idx].Format(b)
+		for idx := range view {
+			vs[idx+1] = view[idx].Format(b)
 		}
 
 		instSuffix = strings.Join(vs, ", ")
 	case OpcodeBrz, OpcodeBrnz:
-		vs := make([]string, len(i.vs)+2)
+		view := i.vs.View()
+		vs := make([]string, len(view)+2)
 		vs[0] = " " + i.v.Format(b)
 		vs[1] = i.blk.(*basicBlock).Name()
-		for idx := range i.vs {
-			vs[idx+2] = i.vs[idx].Format(b)
+		for idx := range view {
+			vs[idx+2] = view[idx].Format(b)
 		}
 		instSuffix = strings.Join(vs, ", ")
 	case OpcodeBrTable:
@@ -2608,7 +2612,7 @@ func (i *Instruction) Format(b Builder) string {
 		rvs = append(rvs, rv.formatWithType(b))
 	}
 
-	for _, v := range i.rValues {
+	for _, v := range i.rValues.View() {
 		rvs = append(rvs, v.formatWithType(b))
 	}
 
@@ -2620,10 +2624,10 @@ func (i *Instruction) Format(b Builder) string {
 }
 
 // addArgumentBranchInst adds an argument to this instruction.
-func (i *Instruction) addArgumentBranchInst(v Value) {
+func (i *Instruction) addArgumentBranchInst(b *builder, v Value) {
 	switch i.opcode {
 	case OpcodeJump, OpcodeBrz, OpcodeBrnz:
-		i.vs = append(i.vs, v)
+		i.vs = i.vs.Append(&b.varLengthPool, v)
 	default:
 		panic("BUG: " + i.opcode.String())
 	}
diff --git a/internal/engine/wazevo/ssa/pass.go b/internal/engine/wazevo/ssa/pass.go
index c5a92b4e..86ec5057 100644
--- a/internal/engine/wazevo/ssa/pass.go
+++ b/internal/engine/wazevo/ssa/pass.go
@@ -85,7 +85,7 @@ func passRedundantPhiEliminationOpt(b *builder) {
 
 			nonSelfReferencingValue := ValueInvalid
 			for predIndex := range blk.preds {
-				pred := blk.preds[predIndex].branch.vs[paramIndex]
+				pred := blk.preds[predIndex].branch.vs.View()[paramIndex]
 				if pred == phiValue {
 					// This is self-referencing: PHI from the same PHI.
 					continue
@@ -122,13 +122,14 @@ func passRedundantPhiEliminationOpt(b *builder) {
 			var cur int
 			predBlk := blk.preds[predIndex]
 			branchInst := predBlk.branch
-			for argIndex, value := range branchInst.vs {
+			view := branchInst.vs.View()
+			for argIndex, value := range view {
 				if _, ok := b.redundantParameterIndexToValue[argIndex]; !ok {
-					branchInst.vs[cur] = value
+					view[cur] = value
 					cur++
 				}
 			}
-			branchInst.vs = branchInst.vs[:cur]
+			branchInst.vs.Cut(cur)
 		}
 
 		// Still need to have the definition of the value of the PHI (previously as the parameter).
diff --git a/internal/engine/wazevo/ssa/vs.go b/internal/engine/wazevo/ssa/vs.go
index 0afc941d..bcbf7a30 100644
--- a/internal/engine/wazevo/ssa/vs.go
+++ b/internal/engine/wazevo/ssa/vs.go
@@ -79,3 +79,9 @@ func (v Value) ID() ValueID {
 func (v Value) setType(typ Type) Value {
 	return v | Value(typ)<<32
 }
+
+// Values is a slice of Value. Use this instead of []Value to reuse the underlying memory.
+type Values = wazevoapi.VarLength[Value]
+
+// ValuesNil is a nil Values.
+var ValuesNil = wazevoapi.NewNilVarLength[Value]()
diff --git a/internal/engine/wazevo/wazevoapi/pool.go b/internal/engine/wazevo/wazevoapi/pool.go
index 7adcd79c..87ae8733 100644
--- a/internal/engine/wazevo/wazevoapi/pool.go
+++ b/internal/engine/wazevo/wazevoapi/pool.go
@@ -107,3 +107,81 @@ func (p *IDedPool[T]) Reset() {
 func (p *IDedPool[T]) MaxIDEncountered() int {
 	return p.maxIDEncountered
 }
+
+const arraySize = 20
+
+// VarLengthPool is a pool of VarLength[T] that can be allocated and reset.
+type VarLengthPool[T any] struct {
+	arrayPool Pool[[arraySize]T]
+	slicePool Pool[[]T]
+}
+
+// VarLength is a variable length array that can be reused via a pool.
+type VarLength[T any] struct {
+	backing *[]T
+}
+
+// NewVarLengthPool returns a new VarLengthPool.
+func NewVarLengthPool[T any]() VarLengthPool[T] {
+	return VarLengthPool[T]{
+		arrayPool: NewPool[[arraySize]T](nil),
+		slicePool: NewPool[[]T](func(i *[]T) {
+			*i = (*i)[:0]
+		}),
+	}
+}
+
+// NewNilVarLength returns a new VarLength[T] with a nil backing.
+func NewNilVarLength[T any]() VarLength[T] {
+	var v *[]T = nil
+	return VarLength[T]{backing: v}
+}
+
+// Allocate allocates a new VarLength[T] from the pool.
+func (p *VarLengthPool[T]) Allocate(knownMin int) VarLength[T] {
+	if knownMin <= arraySize {
+		arr := p.arrayPool.Allocate()[:0]
+		return VarLength[T]{backing: &arr}
+	}
+	slc := p.slicePool.Allocate()
+	return VarLength[T]{backing: slc}
+}
+
+// Reset resets the pool.
+func (p *VarLengthPool[T]) Reset() {
+	p.arrayPool.Reset()
+	p.slicePool.Reset()
+}
+
+// Append appends an item to the backing slice.
+func (i VarLength[T]) Append(p *VarLengthPool[T], item T) VarLength[T] {
+	if i.backing == nil {
+		arr := p.arrayPool.Allocate()[:0]
+		i.backing = &arr
+	}
+
+	if len(*i.backing)+1 <= arraySize {
+		*i.backing = append(*i.backing, item)
+		return i
+	} else if len(*i.backing) == arraySize {
+		slc := p.slicePool.Allocate()
+		// Copy the array to the slice.
+		*slc = append(*slc, *i.backing...)
+	}
+	*i.backing = append(*i.backing, item)
+	return i
+}
+
+// View returns the backing slice.
+func (i VarLength[T]) View() []T {
+	if i.backing == nil {
+		return nil
+	}
+	return *i.backing
+}
+
+// Cut cuts the backing slice to the given length.
+// Precondition: n <= len(i.backing).
+func (i VarLength[T]) Cut(n int) {
+	*i.backing = (*i.backing)[:n]
+}


Test patch:
diff --git a/internal/engine/wazevo/backend/isa/amd64/machine_test.go b/internal/engine/wazevo/backend/isa/amd64/machine_test.go
index 588cc8cb..c1ecad7a 100644
--- a/internal/engine/wazevo/backend/isa/amd64/machine_test.go
+++ b/internal/engine/wazevo/backend/isa/amd64/machine_test.go
@@ -69,7 +69,7 @@ func TestMachine_getOperand_Reg(t *testing.T) {
 				c := builder.AllocateInstruction()
 				sig := &ssa.Signature{Results: []ssa.Type{ssa.TypeI64}}
 				builder.DeclareSignature(sig)
-				c.AsCall(ssa.FuncRef(0), sig, nil)
+				c.AsCall(ssa.FuncRef(0), sig, ssa.ValuesNil)
 				builder.InsertInstruction(c)
 				r := c.Return()
 				ctx.vRegMap[r] = regalloc.VReg(50)
@@ -83,7 +83,7 @@ func TestMachine_getOperand_Reg(t *testing.T) {
 				c := builder.AllocateInstruction()
 				sig := &ssa.Signature{Results: []ssa.Type{ssa.TypeI64, ssa.TypeF64, ssa.TypeF64}}
 				builder.DeclareSignature(sig)
-				c.AsCall(ssa.FuncRef(0), sig, nil)
+				c.AsCall(ssa.FuncRef(0), sig, ssa.ValuesNil)
 				builder.InsertInstruction(c)
 				_, rs := c.Returns()
 				ctx.vRegMap[rs[1]] = regalloc.VReg(50)
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr_operands_test.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr_operands_test.go
index 914d256b..11494652 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr_operands_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr_operands_test.go
@@ -89,7 +89,7 @@ func TestMachine_getOperand_NR(t *testing.T) {
 				c := builder.AllocateInstruction()
 				sig := &ssa.Signature{Results: []ssa.Type{ssa.TypeI64, ssa.TypeF64, ssa.TypeF64}}
 				builder.DeclareSignature(sig)
-				c.AsCall(ssa.FuncRef(0), sig, nil)
+				c.AsCall(ssa.FuncRef(0), sig, ssa.ValuesNil)
 				builder.InsertInstruction(c)
 				_, rs := c.Returns()
 				ctx.vRegMap[rs[1]] = regalloc.VReg(50)
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
index 4289b6a4..350b5760 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
@@ -52,9 +52,9 @@ func TestMachine_LowerConditionalBranch(t *testing.T) {
 		ctx.definitions[cmpVal] = &backend.SSAValueDefinition{Instr: cmpInstr}
 		b := builder.AllocateInstruction()
 		if brz {
-			b.AsBrz(cmpVal, nil, builder.AllocateBasicBlock())
+			b.AsBrz(cmpVal, ssa.ValuesNil, builder.AllocateBasicBlock())
 		} else {
-			b.AsBrnz(cmpVal, nil, builder.AllocateBasicBlock())
+			b.AsBrnz(cmpVal, ssa.ValuesNil, builder.AllocateBasicBlock())
 		}
 		builder.InsertInstruction(b)
 		return b, func(t *testing.T) {
@@ -85,9 +85,9 @@ func TestMachine_LowerConditionalBranch(t *testing.T) {
 		ctx.vRegMap[v1], ctx.vRegMap[v2], ctx.vRegMap[icmpVal] = intToVReg(1), intToVReg(2), intToVReg(3)
 		b := builder.AllocateInstruction()
 		if brz {
-			b.AsBrz(icmpVal, nil, builder.AllocateBasicBlock())
+			b.AsBrz(icmpVal, ssa.ValuesNil, builder.AllocateBasicBlock())
 		} else {
-			b.AsBrnz(icmpVal, nil, builder.AllocateBasicBlock())
+			b.AsBrnz(icmpVal, ssa.ValuesNil, builder.AllocateBasicBlock())
 		}
 		builder.InsertInstruction(b)
 		return b, func(t *testing.T) {
@@ -115,7 +115,7 @@ func TestMachine_LowerConditionalBranch(t *testing.T) {
 				ctx.vRegMap[v1], ctx.vRegMap[v2], ctx.vRegMap[icmpVal] = intToVReg(1), intToVReg(2), intToVReg(3)
 
 				brz := builder.AllocateInstruction()
-				brz.AsBrz(icmpVal, nil, builder.AllocateBasicBlock())
+				brz.AsBrz(icmpVal, ssa.ValuesNil, builder.AllocateBasicBlock())
 				builder.InsertInstruction(brz)
 
 				// Indicate that currently compiling in the different group.
@@ -208,7 +208,7 @@ func TestMachine_LowerSingleBranch(t *testing.T) {
 			name: "jump-fallthrough",
 			setup: func(ctx *mockCompiler, builder ssa.Builder, m *machine) (instr *ssa.Instruction) {
 				jump := builder.AllocateInstruction()
-				jump.AsJump(nil, builder.AllocateBasicBlock())
+				jump.AsJump(ssa.ValuesNil, builder.AllocateBasicBlock())
 				builder.InsertInstruction(jump)
 				jump.AsFallthroughJump()
 				return jump
@@ -220,7 +220,7 @@ func TestMachine_LowerSingleBranch(t *testing.T) {
 			setup: func(ctx *mockCompiler, builder ssa.Builder, m *machine) (instr *ssa.Instruction) {
 				m.executableContext.StartLoweringFunction(10)
 				jump := builder.AllocateInstruction()
-				jump.AsJump(nil, builder.AllocateBasicBlock())
+				jump.AsJump(ssa.ValuesNil, builder.AllocateBasicBlock())
 				builder.InsertInstruction(jump)
 				return jump
 			},
@@ -231,7 +231,7 @@ func TestMachine_LowerSingleBranch(t *testing.T) {
 			setup: func(ctx *mockCompiler, builder ssa.Builder, m *machine) (instr *ssa.Instruction) {
 				m.executableContext.StartLoweringFunction(10)
 				jump := builder.AllocateInstruction()
-				jump.AsJump(nil, builder.ReturnBlock())
+				jump.AsJump(ssa.ValuesNil, builder.ReturnBlock())
 				builder.InsertInstruction(jump)
 				return jump
 			},
diff --git a/internal/engine/wazevo/ssa/builder_test.go b/internal/engine/wazevo/ssa/builder_test.go
index 04355295..26cbdf6f 100644
--- a/internal/engine/wazevo/ssa/builder_test.go
+++ b/internal/engine/wazevo/ssa/builder_test.go
@@ -10,7 +10,7 @@ func Test_maybeInvertBranch(t *testing.T) {
 	insertJump := func(b *builder, src, dst *basicBlock) {
 		b.SetCurrentBlock(src)
 		jump := b.AllocateInstruction()
-		jump.AsJump(nil, dst)
+		jump.AsJump(ValuesNil, dst)
 		b.InsertInstruction(jump)
 	}
 
@@ -21,7 +21,7 @@ func Test_maybeInvertBranch(t *testing.T) {
 		b.InsertInstruction(vinst)
 		v := vinst.Return()
 		brz := b.AllocateInstruction()
-		brz.AsBrz(v, nil, dst)
+		brz.AsBrz(v, ValuesNil, dst)
 		b.InsertInstruction(brz)
 	}
 
@@ -183,10 +183,10 @@ func TestBuilder_splitCriticalEdge(t *testing.T) {
 	b.InsertInstruction(inst)
 	v := inst.Return()
 	originalBrz := b.AllocateInstruction() // This is the split edge.
-	originalBrz.AsBrz(v, nil, dummyBlk)
+	originalBrz.AsBrz(v, ValuesNil, dummyBlk)
 	b.InsertInstruction(originalBrz)
 	dummyJump := b.AllocateInstruction()
-	dummyJump.AsJump(nil, dummyBlk2)
+	dummyJump.AsJump(ValuesNil, dummyBlk2)
 	b.InsertInstruction(dummyJump)
 
 	predInfo := &basicBlockPredecessorInfo{blk: predBlk, branch: originalBrz}
@@ -276,7 +276,11 @@ func TestBuilder_LayoutBlocks(t *testing.T) {
 	insertJump := func(b *builder, src, dst *basicBlock, vs ...Value) {
 		b.SetCurrentBlock(src)
 		jump := b.AllocateInstruction()
-		jump.AsJump(vs, dst)
+		args := b.varLengthPool.Allocate(len(vs))
+		for _, v := range vs {
+			args = args.Append(&b.varLengthPool, v)
+		}
+		jump.AsJump(args, dst)
 		b.InsertInstruction(jump)
 	}
 
@@ -285,7 +289,11 @@ func TestBuilder_LayoutBlocks(t *testing.T) {
 		vinst := b.AllocateInstruction().AsIconst32(0)
 		b.InsertInstruction(vinst)
 		brz := b.AllocateInstruction()
-		brz.AsBrz(condVal, vs, dst)
+		args := b.varLengthPool.Allocate(len(vs))
+		for _, v := range vs {
+			args = args.Append(&b.varLengthPool, v)
+		}
+		brz.AsBrz(condVal, args, dst)
 		b.InsertInstruction(brz)
 	}
 
@@ -536,7 +544,9 @@ func TestBuilder_LayoutBlocks(t *testing.T) {
 				}
 				b.SetCurrentBlock(b1)
 				{
-					b.AllocateInstruction().AsReturn([]Value{retval}).Insert(b)
+					args := b.varLengthPool.Allocate(1)
+					args = args.Append(&b.varLengthPool, retval)
+					b.AllocateInstruction().AsReturn(args).Insert(b)
 				}
 				b.SetCurrentBlock(b2)
 				{
diff --git a/internal/engine/wazevo/ssa/pass_test.go b/internal/engine/wazevo/ssa/pass_test.go
index 2a831a67..261f3225 100644
--- a/internal/engine/wazevo/ssa/pass_test.go
+++ b/internal/engine/wazevo/ssa/pass_test.go
@@ -38,25 +38,25 @@ func TestBuilder_passes(t *testing.T) {
 				b.SetCurrentBlock(entry)
 				{
 					brz := b.AllocateInstruction()
-					brz.AsBrz(value, nil, middle1)
+					brz.AsBrz(value, ValuesNil, middle1)
 					b.InsertInstruction(brz)
 
 					jmp := b.AllocateInstruction()
-					jmp.AsJump(nil, middle2)
+					jmp.AsJump(ValuesNil, middle2)
 					b.InsertInstruction(jmp)
 				}
 
 				b.SetCurrentBlock(middle1)
 				{
 					jmp := b.AllocateInstruction()
-					jmp.AsJump(nil, end)
+					jmp.AsJump(ValuesNil, end)
 					b.InsertInstruction(jmp)
 				}
 
 				b.SetCurrentBlock(middle2)
 				{
 					jmp := b.AllocateInstruction()
-					jmp.AsJump(nil, end)
+					jmp.AsJump(ValuesNil, end)
 					b.InsertInstruction(jmp)
 				}
 
@@ -64,14 +64,14 @@ func TestBuilder_passes(t *testing.T) {
 					unreachable := b.AllocateBasicBlock()
 					b.SetCurrentBlock(unreachable)
 					jmp := b.AllocateInstruction()
-					jmp.AsJump(nil, end)
+					jmp.AsJump(ValuesNil, end)
 					b.InsertInstruction(jmp)
 				}
 
 				b.SetCurrentBlock(end)
 				{
 					jmp := b.AllocateInstruction()
-					jmp.AsJump(nil, middle1)
+					jmp.AsJump(ValuesNil, middle1)
 					b.InsertInstruction(jmp)
 				}
 
@@ -131,7 +131,9 @@ blk3: () <-- (blk1,blk2)
 					b.DefineVariable(var1, iConst, entry)
 
 					jmp := b.AllocateInstruction()
-					jmp.AsJump([]Value{iConst}, loopHeader)
+					args := b.varLengthPool.Allocate(1)
+					args = args.Append(&b.varLengthPool, iConst)
+					jmp.AsJump(args, loopHeader)
 					b.InsertInstruction(jmp)
 				}
 				b.Seal(entry)
@@ -147,12 +149,14 @@ blk3: () <-- (blk1,blk2)
 					b.InsertInstruction(tmpInst)
 					tmp := tmpInst.Return()
 
+					args := b.varLengthPool.Allocate(0)
+					args = args.Append(&b.varLengthPool, tmp)
 					brz := b.AllocateInstruction()
-					brz.AsBrz(value, []Value{tmp}, loopHeader) // Loop to itself.
+					brz.AsBrz(value, args, loopHeader) // Loop to itself.
 					b.InsertInstruction(brz)
 
 					jmp := b.AllocateInstruction()
-					jmp.AsJump(nil, end)
+					jmp.AsJump(ValuesNil, end)
 					b.InsertInstruction(jmp)
 				}
 				b.Seal(loopHeader)
@@ -160,7 +164,7 @@ blk3: () <-- (blk1,blk2)
 				b.SetCurrentBlock(end)
 				{
 					ret := b.AllocateInstruction()
-					ret.AsReturn(nil)
+					ret.AsReturn(ValuesNil)
 					b.InsertInstruction(ret)
 				}
 				return nil
@@ -219,7 +223,7 @@ blk2: () <-- (blk1)
 				refOnceVal := iconstRefOnceInst.Return()
 
 				jmp := b.AllocateInstruction()
-				jmp.AsJump(nil, end)
+				jmp.AsJump(ValuesNil, end)
 				b.InsertInstruction(jmp)
 
 				b.SetCurrentBlock(end)
@@ -233,7 +237,9 @@ blk2: () <-- (blk1)
 				addRes := add.Return()
 
 				ret := b.AllocateInstruction()
-				ret.AsReturn([]Value{addRes})
+				args := b.varLengthPool.Allocate(1)
+				args = args.Append(&b.varLengthPool, addRes)
+				ret.AsReturn(args)
 				b.InsertInstruction(ret)
 				return func(t *testing.T) {
 					// Group IDs.
@@ -311,7 +317,12 @@ blk1: () <-- (blk0)
 				nonZeroSshr := b.AllocateInstruction().AsSshr(i64Param, nonZeroI64).Insert(b).Return()
 
 				ret := b.AllocateInstruction()
-				ret.AsReturn([]Value{nopIshl, nopUshr, nonZeroIshl, nonZeroSshr})
+				args := b.varLengthPool.Allocate(4)
+				args = args.Append(&b.varLengthPool, nopIshl)
+				args = args.Append(&b.varLengthPool, nopUshr)
+				args = args.Append(&b.varLengthPool, nonZeroIshl)
+				args = args.Append(&b.varLengthPool, nonZeroSshr)
+				ret.AsReturn(args)
 				b.InsertInstruction(ret)
 				return nil
 			},
diff --git a/internal/engine/wazevo/wazevoapi/pool_test.go b/internal/engine/wazevo/wazevoapi/pool_test.go
new file mode 100644
index 00000000..e76aa060
--- /dev/null
+++ b/internal/engine/wazevo/wazevoapi/pool_test.go
@@ -0,0 +1,82 @@
+package wazevoapi
+
+import (
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestNewNilVarLength(t *testing.T) {
+	v := NewNilVarLength[uint64]()
+	require.NotNil(t, v)
+	pool := NewVarLengthPool[uint64]()
+	v = v.Append(&pool, 1)
+	require.Equal(t, []uint64{1}, v.View())
+}
+
+func TestAllocate(t *testing.T) {
+	pool := NewVarLengthPool[uint64]()
+	// Array:
+	v := pool.Allocate(10)
+	require.NotNil(t, v.backing)
+	require.Equal(t, 0, len(*v.backing))
+	require.Equal(t, arraySize, cap(*v.backing))
+
+	// Slice backed:
+	v = pool.Allocate(25)
+	require.NotNil(t, v.backing)
+	require.Equal(t, 0, len(*v.backing))
+	v.Append(&pool, 1)
+	require.NotNil(t, v.backing)
+	require.Equal(t, 1, len(*v.backing))
+	v.Append(&pool, 2)
+	require.NotNil(t, v.backing)
+	require.Equal(t, 2, len(*v.backing))
+	capacity := cap(*v.backing)
+
+	// Reset the pool and ensure the backing slice is reused.
+	pool.Reset()
+
+	v = pool.Allocate(10)
+	require.NotNil(t, v.backing)
+	require.Equal(t, 0, len(*v.backing))
+	require.Equal(t, arraySize, cap(*v.backing))
+	v = pool.Allocate(25)
+	require.NotNil(t, v.backing)
+	require.Equal(t, 0, len(*v.backing))
+	require.Equal(t, capacity, cap(*v.backing))
+}
+
+func TestAppendAndView(t *testing.T) {
+	pool := NewVarLengthPool[uint64]()
+	t.Run("zero start", func(t *testing.T) {
+		v := pool.Allocate(0)
+		v.Append(&pool, 1)
+		require.Equal(t, []uint64{1}, v.View())
+	})
+	t.Run("non zero start", func(t *testing.T) {
+		v := pool.Allocate(10)
+		for i := uint64(0); i < 10; i++ {
+			v.Append(&pool, i)
+		}
+		require.Equal(t, []uint64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, v.View())
+		for i := uint64(0); i < 20; i++ {
+			v.Append(&pool, i)
+		}
+		require.Equal(t, []uint64{
+			0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+			0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
+			0xa, 0xb, 0xc, 0xd, 0xe, 0xf, 0x10, 0x11, 0x12, 0x13,
+		}, v.View())
+	})
+}
+
+func TestCut(t *testing.T) {
+	pool := NewVarLengthPool[uint64]()
+	v := pool.Allocate(10)
+	for i := uint64(0); i < 10; i++ {
+		v.Append(&pool, i)
+	}
+	v.Cut(5)
+	require.Equal(t, []uint64{0, 1, 2, 3, 4}, v.View())
+}