Repository: tetratelabs/wazero
Pull request #1142 (2023-02-20 22:57:54)

wazeroir: uses u64 as unique keys for labels
what a low-hanging fruit! 

```
name         old time/op    new time/op    delta
_compile-10     1.04s ± 2%     0.93s ± 2%  -10.52%  (p=0.000 n=18+19)

name         old alloc/op   new alloc/op   delta
_compile-10     646MB ± 0%     609MB ± 0%   -5.67%  (p=0.000 n=18+20)

name         old allocs/op  new allocs/op  delta
_compile-10     10.7M ± 0%      9.4M ± 0%  -12.59%  (p=0.000 n=20+20)
```


Patch:
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index bed412dd..f3011bd4 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -89,11 +89,9 @@ type amd64Compiler struct {
 	// and each item is either placed in register or the actual memory stack.
 	locationStack *runtimeValueLocationStack
 	// labels hold per wazeroir label specific information in this function.
-	labels map[string]*amd64LabelInfo
+	labels map[wazeroir.LabelID]*amd64LabelInfo
 	// stackPointerCeil is the greatest stack pointer value (from runtimeValueLocationStack) seen during compilation.
 	stackPointerCeil uint64
-	// currentLabel holds a currently compiled wazeroir label key. For debugging only.
-	currentLabel string
 	// onStackPointerCeilDeterminedCallBack hold a callback which are called when the max stack pointer is determined BEFORE generating native code.
 	onStackPointerCeilDeterminedCallBack func(stackPointerCeil uint64)
 	withListener                         bool
@@ -113,11 +111,10 @@ func (c *amd64Compiler) Init(ir *wazeroir.CompilationResult, withListener bool)
 	assembler.Reset()
 	vstack.reset()
 	*c = amd64Compiler{
-		labels:       map[string]*amd64LabelInfo{},
+		labels:       map[wazeroir.LabelID]*amd64LabelInfo{},
 		ir:           ir,
 		cpuFeatures:  c.cpuFeatures,
 		withListener: withListener,
-		currentLabel: wazeroir.EntrypointLabel,
 	}
 	c.assembler, c.locationStack = assembler, vstack
 }
@@ -161,13 +158,13 @@ type amd64LabelInfo struct {
 	labelBeginningCallbacks []func(asm.Node)
 }
 
-func (c *amd64Compiler) label(labelKey string) *amd64LabelInfo {
-	ret, ok := c.labels[labelKey]
+func (c *amd64Compiler) label(labelID wazeroir.LabelID) *amd64LabelInfo {
+	ret, ok := c.labels[labelID]
 	if ok {
 		return ret
 	}
-	c.labels[labelKey] = &amd64LabelInfo{}
-	return c.labels[labelKey]
+	c.labels[labelID] = &amd64LabelInfo{}
+	return c.labels[labelID]
 }
 
 // compileBuiltinFunctionCheckExitCode implements compiler.compileBuiltinFunctionCheckExitCode for the amd64 architecture.
@@ -390,8 +387,8 @@ func (c *amd64Compiler) branchInto(target wazeroir.Label) error {
 	if target.IsReturnTarget() {
 		return c.compileReturnFunction()
 	} else {
-		labelKey := target.String()
-		if c.ir.LabelCallers[labelKey] > 1 {
+		labelID := target.ID()
+		if c.ir.LabelCallers[labelID] > 1 {
 			// We can only re-use register state if when there's a single call-site.
 			// Release existing values on registers to the stack if there's multiple ones to have
 			// the consistent value location state at the beginning of label.
@@ -402,14 +399,14 @@ func (c *amd64Compiler) branchInto(target wazeroir.Label) error {
 		// Set the initial stack of the target label, so we can start compiling the label
 		// with the appropriate value locations. Note we clone the stack here as we maybe
 		// manipulate the stack before compiler reaches the label.
-		targetLabel := c.label(labelKey)
+		targetLabel := c.label(labelID)
 		if targetLabel.initialStack == nil {
 			// It seems unnecessary to clone as branchInto is always the tail of the current block.
 			// TODO: verify ^^.
 			targetLabel.initialStack = c.locationStack.clone()
 		}
 		jmp := c.assembler.CompileJump(amd64.JMP)
-		c.assignJumpTarget(labelKey, jmp)
+		c.assignJumpTarget(labelID, jmp)
 	}
 	return nil
 }
@@ -487,8 +484,8 @@ func (c *amd64Compiler) compileBrIf(o wazeroir.OperationBrIf) error {
 			return err
 		}
 	} else {
-		elseLabelKey := elseTarget.Target.String()
-		if c.ir.LabelCallers[elseLabelKey] > 1 {
+		elseLabelID := elseTarget.Target.ID()
+		if c.ir.LabelCallers[elseLabelID] > 1 {
 			// We can only re-use register state if when there's a single call-site.
 			// Release existing values on registers to the stack if there's multiple ones to have
 			// the consistent value location state at the beginning of label.
@@ -499,13 +496,13 @@ func (c *amd64Compiler) compileBrIf(o wazeroir.OperationBrIf) error {
 		// Set the initial stack of the target label, so we can start compiling the label
 		// with the appropriate value locations. Note we clone the stack here as we maybe
 		// manipulate the stack before compiler reaches the label.
-		labelInfo := c.label(elseLabelKey)
+		labelInfo := c.label(elseLabelID)
 		if labelInfo.initialStack == nil {
 			labelInfo.initialStack = c.locationStack
 		}
 
 		elseJmp := c.assembler.CompileJump(amd64.JMP)
-		c.assignJumpTarget(elseLabelKey, elseJmp)
+		c.assignJumpTarget(elseLabelID, elseJmp)
 	}
 
 	// Handle then branch.
@@ -517,8 +514,8 @@ func (c *amd64Compiler) compileBrIf(o wazeroir.OperationBrIf) error {
 	if thenTarget.Target.IsReturnTarget() {
 		return c.compileReturnFunction()
 	} else {
-		thenLabelKey := thenTarget.Target.String()
-		if c.ir.LabelCallers[thenLabelKey] > 1 {
+		thenLabelID := thenTarget.Target.ID()
+		if c.ir.LabelCallers[thenLabelID] > 1 {
 			// We can only re-use register state if when there's a single call-site.
 			// Release existing values on registers to the stack if there's multiple ones to have
 			// the consistent value location state at the beginning of label.
@@ -529,12 +526,12 @@ func (c *amd64Compiler) compileBrIf(o wazeroir.OperationBrIf) error {
 		// Set the initial stack of the target label, so we can start compiling the label
 		// with the appropriate value locations. Note we clone the stack here as we maybe
 		// manipulate the stack before compiler reaches the label.
-		labelInfo := c.label(thenLabelKey)
+		labelInfo := c.label(thenLabelID)
 		if labelInfo.initialStack == nil {
 			labelInfo.initialStack = c.locationStack
 		}
 		thenJmp := c.assembler.CompileJump(amd64.JMP)
-		c.assignJumpTarget(thenLabelKey, thenJmp)
+		c.assignJumpTarget(thenLabelID, thenJmp)
 		return nil
 	}
 }
@@ -660,8 +657,8 @@ func (c *amd64Compiler) compileBrTable(o wazeroir.OperationBrTable) error {
 	return nil
 }
 
-func (c *amd64Compiler) assignJumpTarget(labelKey string, jmpInstruction asm.Node) {
-	jmpTargetLabel := c.label(labelKey)
+func (c *amd64Compiler) assignJumpTarget(labelID wazeroir.LabelID, jmpInstruction asm.Node) {
+	jmpTargetLabel := c.label(labelID)
 	if jmpTargetLabel.initialInstruction != nil {
 		jmpInstruction.AssignJumpTarget(jmpTargetLabel.initialInstruction)
 	} else {
@@ -673,17 +670,12 @@ func (c *amd64Compiler) assignJumpTarget(labelKey string, jmpInstruction asm.Nod
 
 // compileLabel implements compiler.compileLabel for the amd64 architecture.
 func (c *amd64Compiler) compileLabel(o wazeroir.OperationLabel) (skipLabel bool) {
-	if false {
-		fmt.Printf("[label %s ends]\n\n", c.currentLabel)
-	}
-
-	labelKey := o.Label.String()
-	labelInfo := c.label(labelKey)
+	labelID := o.Label.ID()
+	labelInfo := c.label(labelID)
 
 	// If initialStack is not set, that means this label has never been reached.
 	if labelInfo.initialStack == nil {
 		skipLabel = true
-		c.currentLabel = ""
 		return
 	}
 
@@ -705,11 +697,6 @@ func (c *amd64Compiler) compileLabel(o wazeroir.OperationLabel) (skipLabel bool)
 
 	// Clear for debugging purpose. See the comment in "len(amd64LabelInfo.labelBeginningCallbacks) > 0" block above.
 	labelInfo.labelBeginningCallbacks = nil
-
-	if false {
-		fmt.Printf("[label %s (num callers=%d)]\n%s\n", labelKey, c.ir.LabelCallers[labelKey], c.locationStack)
-	}
-	c.currentLabel = labelKey
 	return
 }
 
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 0e3edd65..c0350dd6 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -23,7 +23,7 @@ type arm64Compiler struct {
 	// and each item is either placed in register or the actual memory stack.
 	locationStack *runtimeValueLocationStack
 	// labels maps a label (e.g. ".L1_then") to *arm64LabelInfo.
-	labels map[string]*arm64LabelInfo
+	labels map[wazeroir.LabelID]*arm64LabelInfo
 	// stackPointerCeil is the greatest stack pointer value (from runtimeValueLocationStack) seen during compilation.
 	stackPointerCeil uint64
 	// onStackPointerCeilDeterminedCallBack hold a callback which are called when the ceil of stack pointer is determined before generating native code.
@@ -42,7 +42,7 @@ func (c *arm64Compiler) Init(ir *wazeroir.CompilationResult, withListener bool)
 	assembler, vstack := c.assembler, c.locationStack
 	assembler.Reset()
 	vstack.reset()
-	*c = arm64Compiler{labels: map[string]*arm64LabelInfo{}, ir: ir, withListener: withListener}
+	*c = arm64Compiler{labels: map[wazeroir.LabelID]*arm64LabelInfo{}, ir: ir, withListener: withListener}
 	c.assembler, c.locationStack = assembler, vstack
 }
 
@@ -137,7 +137,7 @@ type arm64LabelInfo struct {
 	labelBeginningCallbacks []func(asm.Node)
 }
 
-func (c *arm64Compiler) label(labelKey string) *arm64LabelInfo {
+func (c *arm64Compiler) label(labelKey wazeroir.LabelID) *arm64LabelInfo {
 	ret, ok := c.labels[labelKey]
 	if ok {
 		return ret
@@ -433,11 +433,11 @@ func (c *arm64Compiler) compileBuiltinFunctionCheckExitCode() error {
 
 // compileLabel implements compiler.compileLabel for the arm64 architecture.
 func (c *arm64Compiler) compileLabel(o wazeroir.OperationLabel) (skipThisLabel bool) {
-	labelKey := o.Label.String()
-	arm64LabelInfo := c.label(labelKey)
+	labelKey := o.Label.ID()
+	labelInfo := c.label(labelKey)
 
 	// If initialStack is not set, that means this label has never been reached.
-	if arm64LabelInfo.initialStack == nil {
+	if labelInfo.initialStack == nil {
 		skipThisLabel = true
 		return
 	}
@@ -448,14 +448,14 @@ func (c *arm64Compiler) compileLabel(o wazeroir.OperationLabel) (skipThisLabel b
 
 	// Save the instructions so that backward branching
 	// instructions can branch to this label.
-	arm64LabelInfo.initialInstruction = labelBegin
+	labelInfo.initialInstruction = labelBegin
 
 	// Set the initial stack.
-	c.setLocationStack(arm64LabelInfo.initialStack)
+	c.setLocationStack(labelInfo.initialStack)
 
 	// Invoke callbacks to notify the forward branching
 	// instructions can properly branch to this label.
-	for _, cb := range arm64LabelInfo.labelBeginningCallbacks {
+	for _, cb := range labelInfo.labelBeginningCallbacks {
 		cb(labelBegin)
 	}
 	return false
@@ -735,8 +735,8 @@ func (c *arm64Compiler) compileBranchInto(target wazeroir.Label) error {
 	if target.IsReturnTarget() {
 		return c.compileReturnFunction()
 	} else {
-		labelKey := target.String()
-		if c.ir.LabelCallers[labelKey] > 1 {
+		labelID := target.ID()
+		if c.ir.LabelCallers[labelID] > 1 {
 			// We can only re-use register state if when there's a single call-site.
 			// Release existing values on registers to the stack if there's multiple ones to have
 			// the consistent value location state at the beginning of label.
@@ -747,20 +747,20 @@ func (c *arm64Compiler) compileBranchInto(target wazeroir.Label) error {
 		// Set the initial stack of the target label, so we can start compiling the label
 		// with the appropriate value locations. Note we clone the stack here as we maybe
 		// manipulate the stack before compiler reaches the label.
-		targetLabel := c.label(labelKey)
+		targetLabel := c.label(labelID)
 		if targetLabel.initialStack == nil {
 			targetLabel.initialStack = c.locationStack.clone()
 		}
 
 		br := c.assembler.CompileJump(arm64.B)
-		c.assignBranchTarget(labelKey, br)
+		c.assignBranchTarget(labelID, br)
 		return nil
 	}
 }
 
 // assignBranchTarget assigns the given label's initial instruction to the destination of br.
-func (c *arm64Compiler) assignBranchTarget(labelKey string, br asm.Node) {
-	target := c.label(labelKey)
+func (c *arm64Compiler) assignBranchTarget(labelID wazeroir.LabelID, br asm.Node) {
+	target := c.label(labelID)
 	if target.initialInstruction != nil {
 		br.AssignJumpTarget(target.initialInstruction)
 	} else {
diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go
index c8b3f849..206fc902 100644
--- a/internal/engine/interpreter/interpreter.go
+++ b/internal/engine/interpreter/interpreter.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"math"
 	"math/bits"
-	"strings"
 	"sync"
 	"unsafe"
 
@@ -292,8 +291,8 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 	hasSourcePCs := len(ir.IROperationSourceOffsetsInWasmBinary) > 0
 	ops := ir.Operations
 	ret := &code{}
-	labelAddress := map[string]uint64{}
-	onLabelAddressResolved := map[string][]func(addr uint64){}
+	labelAddress := map[wazeroir.LabelID]uint64{}
+	onLabelAddressResolved := map[wazeroir.LabelID][]func(addr uint64){}
 	for i, original := range ops {
 		op := &interpreterOp{kind: original.Kind()}
 		if hasSourcePCs {
@@ -303,13 +302,13 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 		case wazeroir.OperationBuiltinFunctionCheckExitCode:
 		case wazeroir.OperationUnreachable:
 		case wazeroir.OperationLabel:
-			labelKey := o.Label.String()
+			labelID := o.Label.ID()
 			address := uint64(len(ret.body))
-			labelAddress[labelKey] = address
-			for _, cb := range onLabelAddressResolved[labelKey] {
+			labelAddress[labelID] = address
+			for _, cb := range onLabelAddressResolved[labelID] {
 				cb(address)
 			}
-			delete(onLabelAddressResolved, labelKey)
+			delete(onLabelAddressResolved, labelID)
 			// We just ignore the label operation
 			// as we translate branch operations to the direct address jmp.
 			continue
@@ -319,12 +318,12 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 				// Jmp to the end of the possible binary.
 				op.us[0] = math.MaxUint64
 			} else {
-				labelKey := o.Target.String()
-				addr, ok := labelAddress[labelKey]
+				labelID := o.Target.ID()
+				addr, ok := labelAddress[labelID]
 				if !ok {
 					// If this is the forward jump (e.g. to the continuation of if, etc.),
 					// the target is not emitted yet, so resolve the address later.
-					onLabelAddressResolved[labelKey] = append(onLabelAddressResolved[labelKey],
+					onLabelAddressResolved[labelID] = append(onLabelAddressResolved[labelID],
 						func(addr uint64) {
 							op.us[0] = addr
 						},
@@ -342,13 +341,13 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 					// Jmp to the end of the possible binary.
 					op.us[i] = math.MaxUint64
 				} else {
-					labelKey := target.Target.String()
-					addr, ok := labelAddress[labelKey]
+					labelID := target.Target.ID()
+					addr, ok := labelAddress[labelID]
 					if !ok {
 						i := i
 						// If this is the forward jump (e.g. to the continuation of if, etc.),
 						// the target is not emitted yet, so resolve the address later.
-						onLabelAddressResolved[labelKey] = append(onLabelAddressResolved[labelKey],
+						onLabelAddressResolved[labelID] = append(onLabelAddressResolved[labelID],
 							func(addr uint64) {
 								op.us[i] = addr
 							},
@@ -368,13 +367,13 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 					// Jmp to the end of the possible binary.
 					op.us[i] = math.MaxUint64
 				} else {
-					labelKey := target.Target.String()
-					addr, ok := labelAddress[labelKey]
+					labelID := target.Target.ID()
+					addr, ok := labelAddress[labelID]
 					if !ok {
 						i := i // pin index for later resolution
 						// If this is the forward jump (e.g. to the continuation of if, etc.),
 						// the target is not emitted yet, so resolve the address later.
-						onLabelAddressResolved[labelKey] = append(onLabelAddressResolved[labelKey],
+						onLabelAddressResolved[labelID] = append(onLabelAddressResolved[labelID],
 							func(addr uint64) {
 								op.us[i] = addr
 							},
@@ -723,11 +722,11 @@ func (e *engine) lowerIR(ir *wazeroir.CompilationResult) (*code, error) {
 	}
 
 	if len(onLabelAddressResolved) > 0 {
-		keys := make([]string, 0, len(onLabelAddressResolved))
-		for key := range onLabelAddressResolved {
-			keys = append(keys, key)
+		keys := make([]wazeroir.LabelID, 0, len(onLabelAddressResolved))
+		for id := range onLabelAddressResolved {
+			keys = append(keys, id)
 		}
-		return nil, fmt.Errorf("labels are not defined: %s", strings.Join(keys, ","))
+		return nil, fmt.Errorf("labels are not defined: %v", keys)
 	}
 	return ret, nil
 }
diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go
index 756f2f2d..1d9a3757 100644
--- a/internal/wazeroir/compiler.go
+++ b/internal/wazeroir/compiler.go
@@ -235,7 +235,7 @@ type CompilationResult struct {
 	//	)
 	//
 	// This example the label corresponding to `(block i32.const 1111)` is never be reached at runtime because `br 0` exits the function before we reach there
-	LabelCallers map[string]uint32
+	LabelCallers map[LabelID]uint32
 
 	// Signature is the function type of the compilation target function.
 	Signature *wasm.FunctionType
@@ -338,7 +338,7 @@ func compile(enabledFeatures api.CoreFeatures,
 		enabledFeatures:            enabledFeatures,
 		controlFrames:              &controlFrames{},
 		callFrameStackSizeInUint64: callFrameStackSizeInUint64,
-		result:                     CompilationResult{LabelCallers: map[string]uint32{}},
+		result:                     CompilationResult{LabelCallers: map[LabelID]uint32{}},
 		body:                       body,
 		localTypes:                 localTypes,
 		sig:                        sig,
@@ -469,7 +469,7 @@ operatorSwitch:
 
 		// Prep labels for inside and the continuation of this loop.
 		loopLabel := Label{FrameID: frame.frameID, Kind: LabelKindHeader}
-		c.result.LabelCallers[loopLabel.String()]++
+		c.result.LabelCallers[loopLabel.ID()]++
 
 		// Emit the branch operation to enter inside the loop.
 		c.emit(
@@ -517,8 +517,8 @@ operatorSwitch:
 		// Prep labels for if and else of this if.
 		thenLabel := Label{Kind: LabelKindHeader, FrameID: frame.frameID}
 		elseLabel := Label{Kind: LabelKindElse, FrameID: frame.frameID}
-		c.result.LabelCallers[thenLabel.String()]++
-		c.result.LabelCallers[elseLabel.String()]++
+		c.result.LabelCallers[thenLabel.ID()]++
+		c.result.LabelCallers[elseLabel.ID()]++
 
 		// Emit the branch operation to enter the then block.
 		c.emit(
@@ -577,7 +577,7 @@ operatorSwitch:
 		// Prep labels for else and the continuation of this if block.
 		elseLabel := Label{FrameID: frame.frameID, Kind: LabelKindElse}
 		continuationLabel := Label{FrameID: frame.frameID, Kind: LabelKindContinuation}
-		c.result.LabelCallers[continuationLabel.String()]++
+		c.result.LabelCallers[continuationLabel.ID()]++
 
 		// Emit the instructions for exiting the if loop,
 		// and then the initiation of else block.
@@ -609,7 +609,7 @@ operatorSwitch:
 			if frame.kind == controlFrameKindIfWithoutElse {
 				// Emit the else label.
 				elseLabel := Label{Kind: LabelKindElse, FrameID: frame.frameID}
-				c.result.LabelCallers[continuationLabel.String()]++
+				c.result.LabelCallers[continuationLabel.ID()]++
 				c.emit(
 					OperationLabel{Label: elseLabel},
 					OperationBr{Target: continuationLabel},
@@ -652,7 +652,7 @@ operatorSwitch:
 			// This case we have to emit "empty" else label.
 			elseLabel := Label{Kind: LabelKindElse, FrameID: frame.frameID}
 			continuationLabel := Label{Kind: LabelKindContinuation, FrameID: frame.frameID}
-			c.result.LabelCallers[continuationLabel.String()] += 2
+			c.result.LabelCallers[continuationLabel.ID()] += 2
 			c.emit(
 				dropOp,
 				OperationBr{Target: continuationLabel},
@@ -665,7 +665,7 @@ operatorSwitch:
 		case controlFrameKindBlockWithContinuationLabel,
 			controlFrameKindIfWithElse:
 			continuationLabel := Label{Kind: LabelKindContinuation, FrameID: frame.frameID}
-			c.result.LabelCallers[continuationLabel.String()]++
+			c.result.LabelCallers[continuationLabel.ID()]++
 			c.emit(
 				dropOp,
 				OperationBr{Target: continuationLabel},
@@ -696,7 +696,7 @@ operatorSwitch:
 		targetFrame.ensureContinuation()
 		dropOp := OperationDrop{Depth: c.getFrameDropRange(targetFrame, false)}
 		target := targetFrame.asLabel()
-		c.result.LabelCallers[target.String()]++
+		c.result.LabelCallers[target.ID()]++
 		c.emit(
 			dropOp,
 			OperationBr{Target: target},
@@ -721,10 +721,10 @@ operatorSwitch:
 		targetFrame.ensureContinuation()
 		drop := c.getFrameDropRange(targetFrame, false)
 		target := targetFrame.asLabel()
-		c.result.LabelCallers[target.String()]++
+		c.result.LabelCallers[target.ID()]++
 
 		continuationLabel := Label{FrameID: c.nextID(), Kind: LabelKindHeader}
-		c.result.LabelCallers[continuationLabel.String()]++
+		c.result.LabelCallers[continuationLabel.ID()]++
 		c.emit(
 			OperationBrIf{
 				Then: BranchTargetDrop{ToDrop: drop, Target: target},
@@ -770,7 +770,7 @@ operatorSwitch:
 			drop := c.getFrameDropRange(targetFrame, false)
 			target := &BranchTargetDrop{ToDrop: drop, Target: targetFrame.asLabel()}
 			targets[i] = target
-			c.result.LabelCallers[target.Target.String()]++
+			c.result.LabelCallers[target.Target.ID()]++
 		}
 
 		// Prep default target control frame.
@@ -783,7 +783,7 @@ operatorSwitch:
 		defaultTargetFrame.ensureContinuation()
 		defaultTargetDrop := c.getFrameDropRange(defaultTargetFrame, false)
 		defaultTarget := defaultTargetFrame.asLabel()
-		c.result.LabelCallers[defaultTarget.String()]++
+		c.result.LabelCallers[defaultTarget.ID()]++
 
 		c.emit(
 			OperationBrTable{
diff --git a/internal/wazeroir/operations.go b/internal/wazeroir/operations.go
index 1bf0e268..be514393 100644
--- a/internal/wazeroir/operations.go
+++ b/internal/wazeroir/operations.go
@@ -1,6 +1,8 @@
 package wazeroir
 
-import "fmt"
+import (
+	"fmt"
+)
 
 // UnsignedInt represents unsigned 32-bit or 64-bit integers.
 type UnsignedInt byte
@@ -730,6 +732,15 @@ type Label struct {
 	Kind    LabelKind
 }
 
+// LabelID is the unique identifiers for blocks in a single function.
+type LabelID uint64
+
+// ID returns the LabelID for this Label.
+func (l Label) ID() (id LabelID) {
+	id = LabelID(l.Kind) | LabelID(l.FrameID)<<32
+	return
+}
+
 // String implements fmt.Stringer.
 func (l Label) String() (ret string) {
 	switch l.Kind {


Test patch:
diff --git a/internal/engine/compiler/compiler_test.go b/internal/engine/compiler/compiler_test.go
index d7dcaf12..5bd348be 100644
--- a/internal/engine/compiler/compiler_test.go
+++ b/internal/engine/compiler/compiler_test.go
@@ -231,7 +231,7 @@ func (j *compilerEnv) requireNewCompiler(t *testing.T, fn func() compiler, ir *w
 
 	if ir == nil {
 		ir = &wazeroir.CompilationResult{
-			LabelCallers: map[string]uint32{},
+			LabelCallers: map[wazeroir.LabelID]uint32{},
 			Signature:    &wasm.FunctionType{},
 		}
 	}
diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go
index adbd1dd5..f20319e8 100644
--- a/internal/wazeroir/compiler_test.go
+++ b/internal/wazeroir/compiler_test.go
@@ -51,7 +51,7 @@ func TestCompile(t *testing.T) {
 				Operations: []Operation{ // begin with params: []
 					OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Functions:    []uint32{0},
 				Types:        []*wasm.FunctionType{v_v},
 				Signature:    v_v,
@@ -70,7 +70,7 @@ func TestCompile(t *testing.T) {
 				Operations: []Operation{ // begin with params: []
 					OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Functions:    []uint32{0},
 				Types:        []*wasm.FunctionType{v_v},
 				Signature:    v_v,
@@ -108,7 +108,7 @@ func TestCompile(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [$x]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Types: []*wasm.FunctionType{
 					{
 						Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32},
@@ -144,7 +144,7 @@ func TestCompile(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{}},                                       // []
 					OperationBr{Target: Label{Kind: LabelKindReturn}},                             // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Types:        []*wasm.FunctionType{v_v},
 				Functions:    []uint32{0},
 				Signature:    v_v,
@@ -172,7 +172,7 @@ func TestCompile(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{}},                                       // []
 					OperationBr{Target: Label{Kind: LabelKindReturn}},                             // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Types:        []*wasm.FunctionType{v_v},
 				Functions:    []uint32{0},
 				Signature:    v_v,
@@ -196,7 +196,7 @@ func TestCompile(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [$old_size]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Types: []*wasm.FunctionType{{
 					Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32},
 					ParamNumInUint64:  1,
@@ -277,7 +277,7 @@ func TestCompile_Block(t *testing.T) {
 				// Note: i32.add comes after br 0 so is unreachable. Compilation succeeds when it feels like it
 				// shouldn't because the br instruction is stack-polymorphic. In other words, (br 0) substitutes for the
 				// two i32 parameters to add.
-				LabelCallers: map[string]uint32{".L2_cont": 1},
+				LabelCallers: map[LabelID]uint32{Label{Kind: LabelKindContinuation, FrameID: 2}.ID(): 1},
 				Functions:    []uint32{0},
 				Types:        []*wasm.FunctionType{v_v},
 				Signature:    v_v,
@@ -357,7 +357,7 @@ func TestCompile_BulkMemoryOperations(t *testing.T) {
 		HasMemory:        true,
 		UsesMemory:       true,
 		HasDataInstances: true,
-		LabelCallers:     map[string]uint32{},
+		LabelCallers:     map[LabelID]uint32{},
 		Signature:        v_v,
 		Functions:        []wasm.Index{0},
 		Types:            []*wasm.FunctionType{v_v},
@@ -405,7 +405,7 @@ func TestCompile_MultiValue(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{Start: 2, End: 3}}, // [$y, $x]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Signature:    i32i32_i32i32,
 				Functions:    []wasm.Index{0},
 				Types:        []*wasm.FunctionType{i32i32_i32i32},
@@ -448,7 +448,7 @@ func TestCompile_MultiValue(t *testing.T) {
 				},
 				// Note: f64.add comes after br 0 so is unreachable. This is why neither the add, nor its other operand
 				// are in the above compilation result.
-				LabelCallers: map[string]uint32{".L2_cont": 1}, // arbitrary label
+				LabelCallers: map[LabelID]uint32{Label{Kind: LabelKindContinuation, FrameID: 2}.ID(): 1}, // arbitrary label
 				Signature:    v_f64f64,
 				Functions:    []wasm.Index{0},
 				Types:        []*wasm.FunctionType{v_f64f64},
@@ -471,7 +471,7 @@ func TestCompile_MultiValue(t *testing.T) {
 					OperationConstI64{Value: 356},                     // [306, 356]
 					OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 				},
-				LabelCallers: map[string]uint32{},
+				LabelCallers: map[LabelID]uint32{},
 				Signature:    _i32i64,
 				Functions:    []wasm.Index{0},
 				Types:        []*wasm.FunctionType{_i32i64},
@@ -520,10 +520,10 @@ func TestCompile_MultiValue(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // .L2 = [3], .L2_else = [-1]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},
 				},
-				LabelCallers: map[string]uint32{
-					".L2":      1,
-					".L2_cont": 2,
-					".L2_else": 1,
+				LabelCallers: map[LabelID]uint32{
+					Label{Kind: LabelKindHeader, FrameID: 2}.ID():       1,
+					Label{Kind: LabelKindContinuation, FrameID: 2}.ID(): 2,
+					Label{Kind: LabelKindElse, FrameID: 2}.ID():         1,
 				},
 				Signature:  i32_i32,
 				Functions:  []wasm.Index{0},
@@ -577,10 +577,10 @@ func TestCompile_MultiValue(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // .L2 = [3], .L2_else = [-1]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},
 				},
-				LabelCallers: map[string]uint32{
-					".L2":      1,
-					".L2_cont": 2,
-					".L2_else": 1,
+				LabelCallers: map[LabelID]uint32{
+					Label{Kind: LabelKindHeader, FrameID: 2}.ID():       1,
+					Label{Kind: LabelKindContinuation, FrameID: 2}.ID(): 2,
+					Label{Kind: LabelKindElse, FrameID: 2}.ID():         1,
 				},
 				Signature:  i32_i32,
 				Functions:  []wasm.Index{0},
@@ -634,10 +634,10 @@ func TestCompile_MultiValue(t *testing.T) {
 					OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // .L2 = [3], .L2_else = [-1]
 					OperationBr{Target: Label{Kind: LabelKindReturn}},
 				},
-				LabelCallers: map[string]uint32{
-					".L2":      1,
-					".L2_cont": 2,
-					".L2_else": 1,
+				LabelCallers: map[LabelID]uint32{
+					Label{Kind: LabelKindHeader, FrameID: 2}.ID():       1,
+					Label{Kind: LabelKindContinuation, FrameID: 2}.ID(): 2,
+					Label{Kind: LabelKindElse, FrameID: 2}.ID():         1,
 				},
 				Signature:  i32_i32,
 				Functions:  []wasm.Index{0},
@@ -687,7 +687,7 @@ func TestCompile_NonTrappingFloatToIntConversion(t *testing.T) {
 			OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [i32.trunc_sat_f32_s($0)]
 			OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
 		},
-		LabelCallers: map[string]uint32{},
+		LabelCallers: map[LabelID]uint32{},
 		Signature:    f32_i32,
 		Functions:    []wasm.Index{0},
 		Types:        []*wasm.FunctionType{f32_i32},
@@ -718,7 +718,7 @@ func TestCompile_SignExtensionOps(t *testing.T) {
 			OperationDrop{Depth: &InclusiveRange{Start: 1, End: 1}}, // [i32.extend8_s($0)]
 			OperationBr{Target: Label{Kind: LabelKindReturn}},       // return!
 		},
-		LabelCallers: map[string]uint32{},
+		LabelCallers: map[LabelID]uint32{},
 		Signature:    i32_i32,
 		Functions:    []wasm.Index{0},
 		Types:        []*wasm.FunctionType{i32_i32},
@@ -769,7 +769,7 @@ func TestCompile_CallIndirectNonZeroTableIndex(t *testing.T) {
 			OperationBr{Target: Label{Kind: LabelKindReturn}}, // return!
 		},
 		HasTable:     true,
-		LabelCallers: map[string]uint32{},
+		LabelCallers: map[LabelID]uint32{},
 		Signature:    v_v,
 		Functions:    []wasm.Index{0},
 		TableTypes: []wasm.RefType{