Repository: tetratelabs/wazero
Pull request #1798 (2023-10-19 02:07:30)

wazevo(regalloc): simplifies live range management
This finally reduces the comp time of Zig stdlib lib tests into 2min from 18m which is the number I got before the regalloc refafctoring.

#1496 


Patch:
diff --git a/internal/engine/wazevo/backend/regalloc/assign.go b/internal/engine/wazevo/backend/regalloc/assign.go
index b15c31a3..851236c7 100644
--- a/internal/engine/wazevo/backend/regalloc/assign.go
+++ b/internal/engine/wazevo/backend/regalloc/assign.go
@@ -23,20 +23,21 @@ func (a *Allocator) assignRegistersPerBlock(f Function, blk Block, vRegIDToNode
 	blkID := blk.ID()
 	var pc programCounter
 	for instr := blk.InstrIteratorBegin(); instr != nil; instr = blk.InstrIteratorNext() {
-		tree := a.blockInfos[blkID].intervalTree
+		tree := a.blockInfos[blkID].intervalMng
 		a.assignRegistersPerInstr(f, pc, instr, vRegIDToNode, tree)
 		pc += pcStride
 	}
 }
 
-func (a *Allocator) assignRegistersPerInstr(f Function, pc programCounter, instr Instr, vRegIDToNode []*node, tree *intervalTree) {
+func (a *Allocator) assignRegistersPerInstr(f Function, pc programCounter, instr Instr, vRegIDToNode []*node, intervalMng *intervalManager) {
 	if indirect := instr.IsIndirectCall(); instr.IsCall() || indirect {
-		// Only take care of non-real VRegs (e.g. VReg.IsRealReg() == false) since
-		// the real VRegs are already placed in the right registers at this point.
-		tree.collectActiveNonRealVRegsAt(
+		intervalMng.collectActiveNodes(
 			// To find the all the live registers "after" call, we need to add pcDefOffset for search.
 			pc+pcDefOffset,
 			&a.nodes1,
+			// Only take care of non-real VRegs (e.g. VReg.IsRealReg() == false) since
+			// the real VRegs are already placed in the right registers at this point.
+			false,
 		)
 		for _, active := range a.nodes1 {
 			if r := active.r; a.regInfo.isCallerSaved(r) {
@@ -101,19 +102,19 @@ func (a *Allocator) assignRegistersPerInstr(f Function, pc programCounter, instr
 		panic("BUG: multiple def instructions must be special cased")
 	}
 
-	a.handleSpills(f, pc, instr, usesSpills, defSpill, tree)
+	a.handleSpills(f, pc, instr, usesSpills, defSpill, intervalMng)
 	a.vs = usesSpills[:0] // for reuse.
 }
 
 func (a *Allocator) handleSpills(
 	f Function, pc programCounter, instr Instr,
-	usesSpills []VReg, defSpill VReg, tree *intervalTree,
+	usesSpills []VReg, defSpill VReg, intervalMng *intervalManager,
 ) {
 	_usesSpills, _defSpill := len(usesSpills) > 0, defSpill.Valid()
 	switch {
 	case !_usesSpills && !_defSpill: // Nothing to do.
 	case !_usesSpills && _defSpill: // Only definition is spilled.
-		tree.collectActiveRealRegNodesAt(pc+pcDefOffset, &a.nodes1)
+		intervalMng.collectActiveNodes(pc+pcDefOffset, &a.nodes1, true)
 		a.spillHandler.init(a.nodes1, instr)
 
 		r, evictedNode := a.spillHandler.getUnusedOrEvictReg(defSpill.RegType(), a.regInfo)
@@ -129,7 +130,7 @@ func (a *Allocator) handleSpills(
 		f.StoreRegisterAfter(defSpill, instr)
 
 	case _usesSpills:
-		tree.collectActiveRealRegNodesAt(pc, &a.nodes1)
+		intervalMng.collectActiveNodes(pc, &a.nodes1, true)
 		a.spillHandler.init(a.nodes1, instr)
 
 		var evicted [3]*node
@@ -173,7 +174,7 @@ func (a *Allocator) handleSpills(
 
 			if !defSpill.IsRealReg() {
 				// This case, the destination register type is different from the source registers.
-				tree.collectActiveRealRegNodesAt(pc+pcDefOffset, &a.nodes1)
+				intervalMng.collectActiveNodes(pc+pcDefOffset, &a.nodes1, true)
 				a.spillHandler.init(a.nodes1, instr)
 				r, evictedNode := a.spillHandler.getUnusedOrEvictReg(defSpill.RegType(), a.regInfo)
 				if evictedNode != nil {
diff --git a/internal/engine/wazevo/backend/regalloc/interval_tree.go b/internal/engine/wazevo/backend/regalloc/interval_tree.go
deleted file mode 100644
index 9dd9475f..00000000
--- a/internal/engine/wazevo/backend/regalloc/interval_tree.go
+++ /dev/null
@@ -1,157 +0,0 @@
-package regalloc
-
-import "github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
-
-type intervalTree struct {
-	root      *intervalTreeNode
-	allocator intervalTreeNodeAllocator
-	intervals map[uint64]*intervalTreeNode
-}
-
-func intervalTreeNodeKey(begin, end programCounter) uint64 {
-	return uint64(begin) | uint64(end)<<32
-}
-
-func (t *intervalTree) insert(n *node, begin, end programCounter) *intervalTreeNode {
-	key := uint64(begin) | uint64(end)<<32
-	if i, ok := t.intervals[key]; ok {
-		i.nodes = append(i.nodes, n)
-		return i
-	}
-	t.root = t.root.insert(t, n, begin, end)
-	ret := t.intervals[key]
-	t.buildNeighbors(ret) // TODO: this can be done while inserting.
-	return ret
-}
-
-func (t *intervalTree) reset() {
-	t.root = nil
-	t.allocator.Reset()
-	t.intervals = make(map[uint64]*intervalTreeNode)
-}
-
-func newIntervalTree() *intervalTree {
-	return &intervalTree{
-		allocator: wazevoapi.NewPool[intervalTreeNode](resetIntervalTreeNode),
-		intervals: make(map[uint64]*intervalTreeNode),
-	}
-}
-
-type intervalTreeNodeAllocator = wazevoapi.Pool[intervalTreeNode]
-
-type intervalTreeNode struct {
-	begin, end  programCounter
-	nodes       []*node
-	maxEnd      programCounter
-	neighbors   []*intervalTreeNode
-	left, right *intervalTreeNode
-	// TODO: color for red-black balancing.
-}
-
-func resetIntervalTreeNode(i *intervalTreeNode) {
-	i.begin = 0
-	i.end = 0
-	i.nodes = i.nodes[:0]
-	i.maxEnd = 0
-	i.neighbors = i.neighbors[:0]
-	i.left = nil
-	i.right = nil
-}
-
-func (i *intervalTreeNode) insert(t *intervalTree, n *node, begin, end programCounter) *intervalTreeNode {
-	if i == nil {
-		intervalNode := t.allocator.Allocate()
-		intervalNode.nodes = append(intervalNode.nodes, n)
-		intervalNode.maxEnd = end
-		intervalNode.begin = begin
-		intervalNode.end = end
-		key := intervalTreeNodeKey(begin, end)
-		t.intervals[key] = intervalNode
-		return intervalNode
-	}
-	if begin < i.begin {
-		i.left = i.left.insert(t, n, begin, end)
-	} else {
-		i.right = i.right.insert(t, n, begin, end)
-	}
-	if i.maxEnd < end {
-		i.maxEnd = end
-	}
-
-	// TODO: balancing logic so that collection functions are faster.
-
-	return i
-}
-
-func (t *intervalTree) buildNeighbors(from *intervalTreeNode) {
-	t.root.buildNeighbors(from)
-}
-
-func (i *intervalTreeNode) buildNeighbors(from *intervalTreeNode) {
-	if i == nil {
-		return
-	}
-	if i.intersects(from) {
-		from.neighbors = append(from.neighbors, i)
-		i.neighbors = append(i.neighbors, from)
-	}
-	if i.left != nil && i.left.maxEnd >= from.begin {
-		i.left.buildNeighbors(from)
-	}
-	if i.begin <= from.end {
-		i.right.buildNeighbors(from)
-	}
-}
-
-func (t *intervalTree) collectActiveNonRealVRegsAt(pc programCounter, overlaps *[]*node) {
-	*overlaps = (*overlaps)[:0]
-	t.root.collectActiveNonRealVRegsAt(pc, overlaps)
-}
-
-func (i *intervalTreeNode) collectActiveNonRealVRegsAt(pc programCounter, overlaps *[]*node) {
-	if i == nil {
-		return
-	}
-	if i.begin <= pc && i.end >= pc {
-		for _, n := range i.nodes {
-			if n.spill() || n.v.IsRealReg() {
-				continue
-			}
-			*overlaps = append(*overlaps, n)
-		}
-	}
-	if i.left != nil && i.left.maxEnd >= pc {
-		i.left.collectActiveNonRealVRegsAt(pc, overlaps)
-	}
-	if i.begin <= pc {
-		i.right.collectActiveNonRealVRegsAt(pc, overlaps)
-	}
-}
-
-func (t *intervalTree) collectActiveRealRegNodesAt(pc programCounter, overlaps *[]*node) {
-	*overlaps = (*overlaps)[:0]
-	t.root.collectActiveRealRegNodesAt(pc, overlaps)
-}
-
-func (i *intervalTreeNode) collectActiveRealRegNodesAt(pc programCounter, overlaps *[]*node) {
-	if i == nil {
-		return
-	}
-	if i.begin <= pc && i.end >= pc {
-		for _, n := range i.nodes {
-			if n.assignedRealReg() != RealRegInvalid {
-				*overlaps = append(*overlaps, n)
-			}
-		}
-	}
-	if i.left != nil && i.left.maxEnd >= pc {
-		i.left.collectActiveRealRegNodesAt(pc, overlaps)
-	}
-	if i.begin <= pc {
-		i.right.collectActiveRealRegNodesAt(pc, overlaps)
-	}
-}
-
-func (i *intervalTreeNode) intersects(j *intervalTreeNode) bool {
-	return i.begin <= j.end && i.end >= j.begin || j.begin <= i.end && j.end >= i.begin
-}
diff --git a/internal/engine/wazevo/backend/regalloc/intervals.go b/internal/engine/wazevo/backend/regalloc/intervals.go
new file mode 100644
index 00000000..6f43e4b9
--- /dev/null
+++ b/internal/engine/wazevo/backend/regalloc/intervals.go
@@ -0,0 +1,147 @@
+package regalloc
+
+import (
+	"sort"
+
+	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
+)
+
+type (
+	// intervalManager manages intervals for each block.
+	intervalManager struct {
+		allocator       wazevoapi.Pool[interval]
+		intervals       map[intervalKey]*interval
+		sortedIntervals []*interval
+		collectionCur   int
+	}
+	// interval represents an interval in the block, which is a range of program counters.
+	// Each interval has a list of nodes which are live in the interval.
+	interval struct {
+		begin, end programCounter
+		// nodes are nodes which are alive in this interval.
+		nodes []*node
+		// neighbors are intervals which are adjacent to this interval.
+		neighbors []*interval
+	}
+	// intervalKey is a key for intervalManager.intervals which consists of begin and end.
+	intervalKey uint64
+)
+
+func newIntervalManager() *intervalManager {
+	return &intervalManager{
+		allocator: wazevoapi.NewPool[interval](resetIntervalTreeNode),
+		intervals: make(map[intervalKey]*interval),
+	}
+}
+
+func resetIntervalTreeNode(i *interval) {
+	i.begin = 0
+	i.end = 0
+	i.nodes = i.nodes[:0]
+	i.neighbors = i.neighbors[:0]
+}
+
+// intervalTreeNodeKey returns a key for intervalManager.intervals.
+func intervalTreeNodeKey(begin, end programCounter) intervalKey {
+	return intervalKey(begin) | intervalKey(end)<<32
+}
+
+// insert inserts a node into the interval tree.
+func (t *intervalManager) insert(n *node, begin, end programCounter) *interval {
+	key := intervalTreeNodeKey(begin, end)
+	if i, ok := t.intervals[key]; ok {
+		i.nodes = append(i.nodes, n)
+		return i
+	}
+	i := t.allocator.Allocate()
+	i.nodes = append(i.nodes, n)
+	i.begin = begin
+	i.end = end
+	t.intervals[key] = i
+	t.sortedIntervals = append(t.sortedIntervals, i) // Will be sorted later.
+	return i
+}
+
+func (t *intervalManager) reset() {
+	t.allocator.Reset()
+	t.sortedIntervals = t.sortedIntervals[:0]
+	t.intervals = make(map[intervalKey]*interval)
+	t.collectionCur = 0
+}
+
+// build is called after all the intervals are inserted. This sorts the intervals,
+// and builds the neighbor intervals for each interval.
+func (t *intervalManager) build() {
+	sort.Slice(t.sortedIntervals, func(i, j int) bool {
+		ii, ij := t.sortedIntervals[i], t.sortedIntervals[j]
+		if ii.begin == ij.begin {
+			return ii.end < ij.end
+		}
+		return ii.begin < ij.begin
+	})
+
+	var cur int
+	var existingEndMax programCounter = -1
+	for i, _interval := range t.sortedIntervals {
+		begin, end := _interval.begin, _interval.end
+		if begin > existingEndMax {
+			cur = i
+			existingEndMax = end
+		} else {
+			for j := cur; j < i; j++ {
+				existing := t.sortedIntervals[j]
+				if existing.end < begin {
+					continue
+				}
+				if existing.begin > end {
+					panic("BUG")
+				}
+				_interval.neighbors = append(_interval.neighbors, existing)
+				existing.neighbors = append(existing.neighbors, _interval)
+			}
+			if end > existingEndMax {
+				existingEndMax = end
+			}
+		}
+	}
+}
+
+// collectActiveNodes collects nodes which are alive at pc, and the result is stored in `collected`.
+// If `real` is true, only nodes which are assigned to a real register are collected.
+func (t *intervalManager) collectActiveNodes(pc programCounter, collected *[]*node, real bool) {
+	*collected = (*collected)[:0]
+
+	// Advance the collection cursor until the current interval's end is greater than pc.
+	l := len(t.sortedIntervals)
+	for cur := t.collectionCur; cur < l; cur++ {
+		curNode := t.sortedIntervals[cur]
+		if curNode.end < pc {
+			t.collectionCur++
+			continue
+		} else {
+			break
+		}
+	}
+
+	for cur := t.collectionCur; cur < l; cur++ {
+		curNode := t.sortedIntervals[cur]
+		if curNode.end < pc {
+			continue
+		} else if curNode.begin > pc {
+			break
+		}
+
+		for _, n := range curNode.nodes {
+			if real {
+				if n.assignedRealReg() == RealRegInvalid {
+					continue
+				}
+			} else {
+				if n.spill() || n.v.IsRealReg() {
+					continue
+				}
+			}
+			*collected = append(*collected, n)
+		}
+	}
+}
diff --git a/internal/engine/wazevo/backend/regalloc/regalloc.go b/internal/engine/wazevo/backend/regalloc/regalloc.go
index 0dc91216..364331bd 100644
--- a/internal/engine/wazevo/backend/regalloc/regalloc.go
+++ b/internal/engine/wazevo/backend/regalloc/regalloc.go
@@ -80,16 +80,16 @@ type (
 		lastUses map[VReg]programCounter
 		kills    map[VReg]programCounter
 		// Pre-colored real registers can have multiple live ranges in one block.
-		realRegUses  map[VReg][]programCounter
-		realRegDefs  map[VReg][]programCounter
-		intervalTree *intervalTree
+		realRegUses map[VReg][]programCounter
+		realRegDefs map[VReg][]programCounter
+		intervalMng *intervalManager
 	}
 
 	// node represents a VReg.
 	node struct {
 		id     int
 		v      VReg
-		ranges []*intervalTreeNode
+		ranges []*interval
 		// r is the real register assigned to this node. It is either a pre-colored register or a register assigned during allocation.
 		r RealReg
 		// neighbors are the nodes that this node interferes with. Such neighbors have the same RegType as this node.
@@ -300,6 +300,7 @@ func (a *Allocator) buildLiveRanges(f Function) {
 		info := a.blockInfoAt(blkID)
 		a.buildLiveRangesForNonReals(info)
 		a.buildLiveRangesForReals(info)
+		info.intervalMng.build()
 	}
 }
 
@@ -334,7 +335,7 @@ func (a *Allocator) buildLiveRangesForNonReals(info *blockInfo) {
 			begin, end = 0, killPos
 		}
 		n := a.getOrAllocateNode(v)
-		intervalNode := info.intervalTree.insert(n, begin, end)
+		intervalNode := info.intervalMng.insert(n, begin, end)
 		n.ranges = append(n.ranges, intervalNode)
 	}
 
@@ -375,7 +376,7 @@ func (a *Allocator) buildLiveRangesForNonReals(info *blockInfo) {
 			}
 		}
 		n := a.getOrAllocateNode(v)
-		intervalNode := info.intervalTree.insert(n, defPos, end)
+		intervalNode := info.intervalMng.insert(n, defPos, end)
 		n.ranges = append(n.ranges, intervalNode)
 	}
 
@@ -431,7 +432,7 @@ func (a *Allocator) buildLiveRangesForReals(info *blockInfo) {
 			n.r = v.RealReg()
 			n.v = v
 			defined, used := defs[i], uses[i]
-			intervalNode := info.intervalTree.insert(n, defined, used)
+			intervalNode := info.intervalMng.insert(n, defined, used)
 			n.ranges = append(n.ranges, intervalNode)
 		}
 	}
@@ -517,10 +518,10 @@ func resetMap[T any](a *Allocator, m map[VReg]T) {
 }
 
 func (a *Allocator) initBlockInfo(i *blockInfo) {
-	if i.intervalTree == nil {
-		i.intervalTree = newIntervalTree()
+	if i.intervalMng == nil {
+		i.intervalMng = newIntervalManager()
 	} else {
-		i.intervalTree.reset()
+		i.intervalMng.reset()
 	}
 	if i.liveOuts == nil {
 		i.liveOuts = make(map[VReg]struct{})


Test patch:
diff --git a/internal/engine/wazevo/backend/regalloc/assign_test.go b/internal/engine/wazevo/backend/regalloc/assign_test.go
index 93b85ea0..cb5285d4 100644
--- a/internal/engine/wazevo/backend/regalloc/assign_test.go
+++ b/internal/engine/wazevo/backend/regalloc/assign_test.go
@@ -11,7 +11,7 @@ func TestAllocator_assignRegistersPerInstr(t *testing.T) {
 		a := NewAllocator(&RegisterInfo{CallerSavedRegisters: [RealRegsNumMax]bool{1: true, 3: true}})
 		pc := programCounter(5)
 
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		liveNodes := []*node{
 			{r: 1, v: 0xa},
 			{r: RealRegInvalid, v: 0xb},           // Spill. not save target.
@@ -20,12 +20,13 @@ func TestAllocator_assignRegistersPerInstr(t *testing.T) {
 			{r: 4, v: 0xd}, // real reg, but not caller saved. not save target
 		}
 		for _, n := range liveNodes {
-			tree.insert(n, 5, 20)
+			manager.insert(n, 5, 20)
 		}
+		manager.build()
 		call := newMockInstr().asCall()
 		blk := newMockBlock(0, call).entry()
 		f := newMockFunction(blk)
-		a.assignRegistersPerInstr(f, pc, call, nil, tree)
+		a.assignRegistersPerInstr(f, pc, call, nil, manager)
 
 		require.Equal(t, 2, len(f.befores))
 		require.Equal(t, 2, len(f.afters))
@@ -36,8 +37,8 @@ func TestAllocator_assignRegistersPerInstr(t *testing.T) {
 		functionPtrVRegID := 0x0
 		functionPtrVReg := VReg(functionPtrVRegID).SetRegType(RegTypeInt)
 		functionPtrLiveNode := &node{r: 0xf, v: functionPtrVReg}
-		tree := newIntervalTree()
-		tree.insert(functionPtrLiveNode, 4, pc)
+		manager := newIntervalManager()
+		manager.insert(functionPtrLiveNode, 4, pc)
 		liveNodes := []*node{
 			{r: 1, v: 0xa},
 			{r: 2, v: FromRealReg(1, RegTypeInt)}, // Real reg-backed VReg. not target
@@ -45,12 +46,13 @@ func TestAllocator_assignRegistersPerInstr(t *testing.T) {
 			{r: 4, v: 0xd}, // real reg, but not caller saved. not save target
 		}
 		for _, n := range liveNodes {
-			tree.insert(n, 5, 20)
+			manager.insert(n, 5, 20)
 		}
+		manager.build()
 		callInd := newMockInstr().asIndirectCall().use(functionPtrVReg)
 		blk := newMockBlock(0, callInd).entry()
 		f := newMockFunction(blk)
-		a.assignRegistersPerInstr(f, pc, callInd, []*node{0: functionPtrLiveNode}, tree)
+		a.assignRegistersPerInstr(f, pc, callInd, []*node{0: functionPtrLiveNode}, manager)
 
 		require.Equal(t, 2, len(f.befores))
 		require.Equal(t, 2, len(f.afters))
@@ -71,17 +73,17 @@ func TestAllocator_assignRegistersPerInstr(t *testing.T) {
 			{r: 3, v: 0xc},
 			{r: 4, v: 0xd}, // real reg, but not caller saved. not save target
 		}
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		for _, n := range liveNodes {
-			tree.insert(n, 5, 20)
+			manager.insert(n, 5, 20)
 		}
-
+		manager.build()
 		callInd := newMockInstr().asIndirectCall().use(functionPtrVReg)
 		blk := newMockBlock(0, callInd).entry()
 		f := newMockFunction(blk)
 		a.assignRegistersPerInstr(f, pc, callInd, []*node{
 			0: {r: RealRegInvalid},
-		}, tree)
+		}, manager)
 
 		require.Equal(t, 3, len(f.befores))
 		require.Equal(t, 2, len(f.afters))
@@ -132,10 +134,11 @@ func TestAllocator_handleSpills(t *testing.T) {
 			{r: RealReg(0xb), v: 0xa},
 			{r: RealReg(0xc), v: 0xc},
 		}
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		for _, n := range liveNodes {
-			tree.insert(n, pc, 20)
+			manager.insert(n, pc, 20)
 		}
+		manager.build()
 		a := NewAllocator(&RegisterInfo{
 			AllocatableRegisters: [3][]RealReg{RegTypeInt: {0xa, 0xb, 0xc}}, // Only live nodes are allocatable.
 		})
@@ -144,7 +147,7 @@ func TestAllocator_handleSpills(t *testing.T) {
 
 		vr := VReg(100).SetRegType(RegTypeInt)
 		instr := newMockInstr().def(vr)
-		a.handleSpills(f, pc, instr, nil, vr, tree)
+		a.handleSpills(f, pc, instr, nil, vr, manager)
 		require.Equal(t, 1, len(instr.defs))
 		require.Equal(t, RealReg(0xa), instr.defs[0].RealReg())
 
@@ -162,11 +165,11 @@ func TestAllocator_handleSpills(t *testing.T) {
 			{r: RealReg(0xb), v: 0xa},
 			{r: RealReg(0xc), v: 0xc},
 		}
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		for _, n := range liveNodes {
-			tree.insert(n, pc, 20)
+			manager.insert(n, pc, 20)
 		}
-
+		manager.build()
 		a := NewAllocator(&RegisterInfo{
 			AllocatableRegisters: [3][]RealReg{RegTypeInt: {0xb, 0xc}}, // Only live nodes are allocatable.
 		})
@@ -175,7 +178,7 @@ func TestAllocator_handleSpills(t *testing.T) {
 
 		vr := VReg(100).SetRegType(RegTypeInt)
 		instr := newMockInstr().def(vr)
-		a.handleSpills(f, pc, instr, nil, vr, tree)
+		a.handleSpills(f, pc, instr, nil, vr, manager)
 		require.Equal(t, 1, len(instr.defs))
 		require.Equal(t, RealReg(0xb), instr.defs[0].RealReg())
 
@@ -194,11 +197,11 @@ func TestAllocator_handleSpills(t *testing.T) {
 			{r: RealReg(0xc), v: 0xc},
 		}
 
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		for _, n := range liveNodes {
-			tree.insert(n, pc, 20)
+			manager.insert(n, pc, 20)
 		}
-
+		manager.build()
 		a := NewAllocator(&RegisterInfo{
 			AllocatableRegisters: [3][]RealReg{RegTypeInt: {0xb, 0xc, 0xf /* free */}},
 		})
@@ -207,7 +210,7 @@ func TestAllocator_handleSpills(t *testing.T) {
 
 		vr := VReg(100).SetRegType(RegTypeInt)
 		instr := newMockInstr().def(vr)
-		a.handleSpills(f, pc, instr, nil, vr, tree)
+		a.handleSpills(f, pc, instr, nil, vr, manager)
 		require.Equal(t, 1, len(instr.defs))
 		require.Equal(t, RealReg(0xf), instr.defs[0].RealReg())
 
@@ -223,11 +226,11 @@ func TestAllocator_handleSpills(t *testing.T) {
 			{r: RealReg(0xc), v: 0xc},
 		}
 
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		for _, n := range liveNodes {
-			tree.insert(n, pc, 20)
+			manager.insert(n, pc, 20)
 		}
-
+		manager.build()
 		a := NewAllocator(&RegisterInfo{
 			AllocatableRegisters: [3][]RealReg{
 				RegTypeInt:   {0xb, 0xc, 0xa /* free */},
@@ -241,7 +244,7 @@ func TestAllocator_handleSpills(t *testing.T) {
 			VReg(102).SetRegType(RegTypeFloat)
 		d1 := VReg(104).SetRegType(RegTypeFloat)
 		instr := newMockInstr().use(u1, u2, u3).def(d1)
-		a.handleSpills(f, pc, instr, []VReg{u1, u3}, d1, tree)
+		a.handleSpills(f, pc, instr, []VReg{u1, u3}, d1, manager)
 		require.Equal(t, []VReg{u1.SetRealReg(0xa), u2, u3.SetRealReg(0xf)}, instr.uses)
 		require.Equal(t, []VReg{d1.SetRealReg(0xf)}, instr.defs)
 
@@ -261,11 +264,11 @@ func TestAllocator_handleSpills(t *testing.T) {
 			{r: RealReg(0xf), v: 0xb},
 		}
 
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		for _, n := range liveNodes {
-			tree.insert(n, pc, 20)
+			manager.insert(n, pc, 20)
 		}
-
+		manager.build()
 		a := NewAllocator(&RegisterInfo{
 			AllocatableRegisters: [3][]RealReg{
 				RegTypeInt:   {0xb, 0xa /* free */},
@@ -277,7 +280,7 @@ func TestAllocator_handleSpills(t *testing.T) {
 		u1 := VReg(100).SetRegType(RegTypeInt)
 		d1 := VReg(104).SetRegType(RegTypeFloat)
 		instr := newMockInstr().use(u1).def(d1)
-		a.handleSpills(f, pc, instr, []VReg{u1}, d1, tree)
+		a.handleSpills(f, pc, instr, []VReg{u1}, d1, manager)
 		require.Equal(t, []VReg{u1.SetRealReg(0xa)}, instr.uses)
 		require.Equal(t, []VReg{d1.SetRealReg(0xf)}, instr.defs)
 
@@ -297,11 +300,11 @@ func TestAllocator_handleSpills(t *testing.T) {
 			{r: RealReg(0xc), v: 0xa},
 			{r: RealReg(0xf), v: 0xb},
 		}
-		tree := newIntervalTree()
+		manager := newIntervalManager()
 		for _, n := range liveNodes {
-			tree.insert(n, pc, 20)
+			manager.insert(n, pc, 20)
 		}
-
+		manager.build()
 		a := NewAllocator(&RegisterInfo{
 			AllocatableRegisters: [3][]RealReg{
 				RegTypeInt:   {0xb, 0xc},
@@ -313,7 +316,7 @@ func TestAllocator_handleSpills(t *testing.T) {
 		u1 := VReg(100).SetRegType(RegTypeInt)
 		d1 := VReg(104).SetRegType(RegTypeFloat)
 		instr := newMockInstr().use(u1).def(d1)
-		a.handleSpills(f, pc, instr, []VReg{u1}, d1, tree)
+		a.handleSpills(f, pc, instr, []VReg{u1}, d1, manager)
 		require.Equal(t, []VReg{u1.SetRealReg(0xb)}, instr.uses)
 		require.Equal(t, []VReg{d1.SetRealReg(0xf)}, instr.defs)
 
diff --git a/internal/engine/wazevo/backend/regalloc/coloring_test.go b/internal/engine/wazevo/backend/regalloc/coloring_test.go
index 9b2a837b..6f8e662a 100644
--- a/internal/engine/wazevo/backend/regalloc/coloring_test.go
+++ b/internal/engine/wazevo/backend/regalloc/coloring_test.go
@@ -259,7 +259,7 @@ func TestAllocator_buildNeighbors(t *testing.T) {
 		{n: newNode(0)},
 		{
 			n: &node{
-				ranges: []*intervalTreeNode{
+				ranges: []*interval{
 					{nodes: newNodes(1, 2, 3)},
 					{nodes: newNodes(4, 5, 1, 2, 3)},
 				},
@@ -268,7 +268,7 @@ func TestAllocator_buildNeighbors(t *testing.T) {
 		},
 		{
 			n: &node{
-				ranges: []*intervalTreeNode{
+				ranges: []*interval{
 					{nodes: newNodes(1, 2, 3)},
 					{nodes: newNodes(1, 2, 3)},
 					{nodes: newNodes(1, 2, 3)},
@@ -280,9 +280,9 @@ func TestAllocator_buildNeighbors(t *testing.T) {
 		},
 		{
 			n: &node{
-				ranges: []*intervalTreeNode{
+				ranges: []*interval{
 					{nodes: newNodes(1, 2, 3)},
-					{nodes: newNodes(4), neighbors: []*intervalTreeNode{
+					{nodes: newNodes(4), neighbors: []*interval{
 						{nodes: newNodes(5, 6)},
 						{nodes: newNodes(100, 200)},
 					}},
diff --git a/internal/engine/wazevo/backend/regalloc/interval_tree_test.go b/internal/engine/wazevo/backend/regalloc/interval_tree_test.go
deleted file mode 100644
index a2b195c4..00000000
--- a/internal/engine/wazevo/backend/regalloc/interval_tree_test.go
+++ /dev/null
@@ -1,385 +0,0 @@
-package regalloc
-
-import (
-	"fmt"
-	"sort"
-	"testing"
-
-	"github.com/tetratelabs/wazero/internal/testing/require"
-)
-
-func TestIntervalTree_reset(t *testing.T) {
-	tree := newIntervalTree()
-	n := tree.allocator.Allocate()
-	tree.root = n
-	tree.reset()
-
-	require.Nil(t, tree.root)
-	require.Equal(t, 0, tree.allocator.Allocated())
-}
-
-func TestIntervalTreeInsert(t *testing.T) {
-	n1 := &node{}
-	tree := newIntervalTree()
-	tree.insert(n1, 100, 200)
-	require.NotNil(t, tree.root)
-	require.NotNil(t, tree.root.nodes)
-	require.Equal(t, n1, tree.root.nodes[0])
-	n, ok := tree.intervals[intervalTreeNodeKey(100, 200)]
-	require.True(t, ok)
-	require.Equal(t, n1, n.nodes[0])
-}
-
-func TestIntervalTreeNodeInsert(t *testing.T) {
-	t.Run("nil", func(t *testing.T) {
-		tree := newIntervalTree()
-		var n *intervalTreeNode
-		allocated := n.insert(tree, &node{}, 0, 100)
-		require.Equal(t, 1, tree.allocator.Allocated())
-		require.NotNil(t, allocated)
-		require.Equal(t, allocated, tree.allocator.View(0))
-		require.Equal(t, programCounter(100), allocated.maxEnd)
-		require.Equal(t, programCounter(0), allocated.begin)
-		require.Equal(t, programCounter(100), allocated.end)
-		require.Equal(t, 1, len(allocated.nodes))
-		n, ok := tree.intervals[intervalTreeNodeKey(0, 100)]
-		require.True(t, ok)
-		require.Equal(t, allocated, n)
-	})
-	t.Run("left", func(t *testing.T) {
-		tree := newIntervalTree()
-		n := &intervalTreeNode{begin: 50, end: 100, maxEnd: 100}
-		n1 := &node{}
-		self := n.insert(tree, n1, 0, 200)
-		require.Equal(t, self, n)
-		require.Equal(t, 1, tree.allocator.Allocated())
-		left := tree.allocator.View(0)
-		require.Equal(t, n.left, left)
-		require.Nil(t, n.right)
-		require.Equal(t, programCounter(200), n.maxEnd)
-		require.Equal(t, left.nodes[0], n1)
-	})
-	t.Run("right", func(t *testing.T) {
-		tree := newIntervalTree()
-		n := &intervalTreeNode{begin: 50, end: 100, maxEnd: 100}
-		n1 := &node{}
-		self := n.insert(tree, n1, 150, 200)
-		require.Equal(t, self, n)
-		require.Equal(t, 1, tree.allocator.Allocated())
-		right := tree.allocator.View(0)
-		require.Equal(t, n.right, right)
-		require.Nil(t, n.left)
-		require.Equal(t, programCounter(200), n.maxEnd)
-		require.Equal(t, right.nodes[0], n1)
-	})
-}
-
-type (
-	interval struct {
-		begin, end programCounter
-		id         int
-	}
-	queryCase struct {
-		query programCounter
-		exp   []int
-	}
-)
-
-func newQueryCase(s programCounter, exp ...int) queryCase {
-	return queryCase{query: s, exp: exp}
-}
-
-func TestIntervalTree_collectActiveNodesAt(t *testing.T) {
-	for _, tc := range []struct {
-		name       string
-		intervals  []interval
-		queryCases []queryCase
-	}{
-		{
-			name:      "single",
-			intervals: []interval{{begin: 0, end: 100, id: 0}},
-			queryCases: []queryCase{
-				newQueryCase(0, 0),
-				newQueryCase(0, 0),
-				newQueryCase(1, 0),
-				newQueryCase(1, 0),
-				newQueryCase(101),
-			},
-		},
-		{
-			name:      "single/2",
-			intervals: []interval{{begin: 50, end: 100, id: 0}},
-			queryCases: []queryCase{
-				newQueryCase(50, 0),
-				newQueryCase(50, 0),
-				newQueryCase(51, 0),
-				newQueryCase(51, 0),
-				newQueryCase(101),
-				newQueryCase(48),
-			},
-		},
-		{
-			name:      "same id for different intervals",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 150, end: 200, id: 0xa}},
-			queryCases: []queryCase{
-				newQueryCase(0),
-				newQueryCase(50, 0xa),
-				newQueryCase(101),
-				newQueryCase(150, 0xa),
-				newQueryCase(101),
-			},
-		},
-		{
-			name:      "two disjoint intervals",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 150, end: 200, id: 0xb}},
-			queryCases: []queryCase{
-				newQueryCase(50, 0xa),
-				newQueryCase(0),
-				newQueryCase(51, 0xa),
-				newQueryCase(101),
-				newQueryCase(150, 0xb),
-				newQueryCase(200, 0xb),
-				newQueryCase(101),
-				newQueryCase(201),
-			},
-		},
-		{
-			name:      "two intersecting intervals",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 51, end: 200, id: 0xb}},
-			queryCases: []queryCase{
-				newQueryCase(0),
-				newQueryCase(70, 0xa, 0xb),
-				newQueryCase(1),
-				newQueryCase(50, 0xa),
-				newQueryCase(51, 0xa, 0xb),
-				newQueryCase(100, 0xa, 0xb),
-				newQueryCase(101, 0xb),
-				newQueryCase(49),
-				newQueryCase(1001),
-			},
-		},
-		{
-			name:      "two enclosing interval",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 25, end: 200, id: 0xb}, {begin: 40, end: 1000, id: 0xc}},
-			queryCases: []queryCase{
-				newQueryCase(24),
-				newQueryCase(25, 0xb),
-				newQueryCase(39, 0xb),
-				newQueryCase(40, 0xb, 0xc),
-				newQueryCase(100, 0xa, 0xb, 0xc),
-				newQueryCase(99, 0xa, 0xb, 0xc),
-				newQueryCase(100, 0xa, 0xb, 0xc),
-				newQueryCase(50, 0xa, 0xb, 0xc),
-				newQueryCase(51, 0xa, 0xb, 0xc),
-				newQueryCase(101, 0xb, 0xc),
-				newQueryCase(49, 0xb, 0xc),
-				newQueryCase(200, 0xb, 0xc),
-				newQueryCase(201, 0xc),
-				newQueryCase(1000, 0xc),
-				newQueryCase(1001),
-			},
-		},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			tree := newIntervalTree()
-			var maxID int
-			for _, inter := range tc.intervals {
-				n := &node{id: inter.id, r: RealReg(1)}
-				tree.insert(n, inter.begin, inter.end)
-				if maxID < inter.id {
-					maxID = inter.id
-				}
-				key := intervalTreeNodeKey(inter.begin, inter.end)
-				inserted := tree.intervals[key]
-				inserted.nodes = append(inserted.nodes, &node{v: VRegInvalid.SetRealReg(RealRegInvalid)}) // non-real reg should be ignored.
-			}
-			for _, qc := range tc.queryCases {
-				t.Run(fmt.Sprintf("%d", qc.query), func(t *testing.T) {
-					var collected []*node
-					tree.collectActiveRealRegNodesAt(qc.query, &collected)
-					require.Equal(t, len(qc.exp), len(collected))
-					var foundIDs []int
-					for _, n := range collected {
-						foundIDs = append(foundIDs, n.id)
-					}
-					sort.Slice(foundIDs, func(i, j int) bool {
-						return foundIDs[i] < foundIDs[j]
-					})
-					require.Equal(t, qc.exp, foundIDs)
-				})
-			}
-		})
-	}
-}
-
-func TestIntervalTree_collectActiveNonRealVRegsAt(t *testing.T) {
-	for _, tc := range []struct {
-		name       string
-		intervals  []interval
-		queryCases []queryCase
-	}{
-		{
-			name:      "single",
-			intervals: []interval{{begin: 0, end: 100, id: 0}},
-			queryCases: []queryCase{
-				newQueryCase(0, 0),
-				newQueryCase(0, 0),
-				newQueryCase(1, 0),
-				newQueryCase(1, 0),
-				newQueryCase(101),
-			},
-		},
-		{
-			name:      "single/2",
-			intervals: []interval{{begin: 50, end: 100, id: 0}},
-			queryCases: []queryCase{
-				newQueryCase(50, 0),
-				newQueryCase(50, 0),
-				newQueryCase(51, 0),
-				newQueryCase(51, 0),
-				newQueryCase(101),
-				newQueryCase(48),
-			},
-		},
-		{
-			name:      "same id for different intervals",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 150, end: 200, id: 0xa}},
-			queryCases: []queryCase{
-				newQueryCase(0),
-				newQueryCase(50, 0xa),
-				newQueryCase(101),
-				newQueryCase(150, 0xa),
-				newQueryCase(101),
-			},
-		},
-		{
-			name:      "two disjoint intervals",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 150, end: 200, id: 0xb}},
-			queryCases: []queryCase{
-				newQueryCase(50, 0xa),
-				newQueryCase(0),
-				newQueryCase(51, 0xa),
-				newQueryCase(101),
-				newQueryCase(150, 0xb),
-				newQueryCase(200, 0xb),
-				newQueryCase(101),
-				newQueryCase(201),
-			},
-		},
-		{
-			name:      "two intersecting intervals",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 51, end: 200, id: 0xb}},
-			queryCases: []queryCase{
-				newQueryCase(0),
-				newQueryCase(70, 0xa, 0xb),
-				newQueryCase(1),
-				newQueryCase(50, 0xa),
-				newQueryCase(51, 0xa, 0xb),
-				newQueryCase(100, 0xa, 0xb),
-				newQueryCase(101, 0xb),
-				newQueryCase(49),
-				newQueryCase(1001),
-			},
-		},
-		{
-			name:      "two enclosing interval",
-			intervals: []interval{{begin: 50, end: 100, id: 0xa}, {begin: 25, end: 200, id: 0xb}, {begin: 40, end: 1000, id: 0xc}},
-			queryCases: []queryCase{
-				newQueryCase(24),
-				newQueryCase(25, 0xb),
-				newQueryCase(39, 0xb),
-				newQueryCase(40, 0xb, 0xc),
-				newQueryCase(100, 0xa, 0xb, 0xc),
-				newQueryCase(99, 0xa, 0xb, 0xc),
-				newQueryCase(100, 0xa, 0xb, 0xc),
-				newQueryCase(50, 0xa, 0xb, 0xc),
-				newQueryCase(51, 0xa, 0xb, 0xc),
-				newQueryCase(101, 0xb, 0xc),
-				newQueryCase(49, 0xb, 0xc),
-				newQueryCase(200, 0xb, 0xc),
-				newQueryCase(201, 0xc),
-				newQueryCase(1000, 0xc),
-				newQueryCase(1001),
-			},
-		},
-	} {
-		t.Run(tc.name, func(t *testing.T) {
-			tree := newIntervalTree()
-			var maxID int
-			for _, inter := range tc.intervals {
-				n := &node{id: inter.id, r: RealReg(1)}
-				tree.insert(n, inter.begin, inter.end)
-				if maxID < inter.id {
-					maxID = inter.id
-				}
-				key := intervalTreeNodeKey(inter.begin, inter.end)
-				inserted := tree.intervals[key]
-				// They are ignored.
-				inserted.nodes = append(inserted.nodes, &node{v: FromRealReg(1, RegTypeInt)})
-				inserted.nodes = append(inserted.nodes, &node{v: FromRealReg(1, RegTypeFloat)})
-				inserted.nodes = append(inserted.nodes, &node{v: VReg(1)})
-			}
-			for _, qc := range tc.queryCases {
-				t.Run(fmt.Sprintf("%d", qc.query), func(t *testing.T) {
-					var collected []*node
-					tree.collectActiveNonRealVRegsAt(qc.query, &collected)
-					require.Equal(t, len(qc.exp), len(collected))
-					var foundIDs []int
-					for _, n := range collected {
-						foundIDs = append(foundIDs, n.id)
-					}
-					sort.Slice(foundIDs, func(i, j int) bool {
-						return foundIDs[i] < foundIDs[j]
-					})
-					require.Equal(t, qc.exp, foundIDs)
-				})
-			}
-		})
-	}
-}
-
-func TestIntervalTreeNode_intersects(t *testing.T) {
-	for _, tc := range []struct {
-		rhs, lhs intervalTreeNode
-		exp      bool
-	}{
-		{
-			rhs: intervalTreeNode{begin: 0, end: 100},
-			lhs: intervalTreeNode{begin: 0, end: 100},
-			exp: true,
-		},
-		{
-			rhs: intervalTreeNode{begin: 0, end: 100},
-			lhs: intervalTreeNode{begin: 0, end: 99},
-			exp: true,
-		},
-		{
-			rhs: intervalTreeNode{begin: 0, end: 100},
-			lhs: intervalTreeNode{begin: 1, end: 100},
-			exp: true,
-		},
-		{
-			rhs: intervalTreeNode{begin: 50, end: 100},
-			lhs: intervalTreeNode{begin: 1, end: 49},
-			exp: false,
-		},
-		{
-			rhs: intervalTreeNode{begin: 50, end: 100},
-			lhs: intervalTreeNode{begin: 1, end: 50},
-			exp: true,
-		},
-		{
-			rhs: intervalTreeNode{begin: 50, end: 100},
-			lhs: intervalTreeNode{begin: 1, end: 51},
-			exp: true,
-		},
-		{
-			rhs: intervalTreeNode{begin: 50, end: 100},
-			lhs: intervalTreeNode{begin: 99, end: 102},
-			exp: true,
-		},
-	} {
-		actual := tc.rhs.intersects(&tc.lhs)
-		require.Equal(t, tc.exp, actual)
-	}
-}
diff --git a/internal/engine/wazevo/backend/regalloc/intervals_test.go b/internal/engine/wazevo/backend/regalloc/intervals_test.go
new file mode 100644
index 00000000..cec878c4
--- /dev/null
+++ b/internal/engine/wazevo/backend/regalloc/intervals_test.go
@@ -0,0 +1,258 @@
+package regalloc
+
+import (
+	"fmt"
+	"sort"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestIntervalsManager_build(t *testing.T) {
+	type (
+		intervalCase struct {
+			begin, end programCounter
+		}
+		expNeighborCase struct {
+			index     int
+			neighbors []intervalCase
+		}
+	)
+
+	for _, tc := range []struct {
+		name         string
+		intervals    []intervalCase
+		expNeighbors []expNeighborCase
+	}{
+		{
+			name:         "single",
+			intervals:    []intervalCase{{begin: 0, end: 100}},
+			expNeighbors: []expNeighborCase{{index: 0}},
+		},
+		{
+			name:         "disjoints",
+			intervals:    []intervalCase{{begin: 50, end: 100}, {begin: 1, end: 2}},
+			expNeighbors: []expNeighborCase{{index: 0}, {index: 1}},
+		},
+		{
+			name:         "disjoints duplicate",
+			intervals:    []intervalCase{{begin: 50, end: 100}, {begin: 1, end: 2}, {begin: 50, end: 100}, {begin: 1, end: 2}},
+			expNeighbors: []expNeighborCase{{index: 0}, {index: 1}, {index: 2}, {index: 3}},
+		},
+		{
+			name: "two intersecting",
+			intervals: []intervalCase{
+				{begin: 70, end: 200},
+				{begin: 50, end: 100},
+			},
+			expNeighbors: []expNeighborCase{
+				{index: 0, neighbors: []intervalCase{{begin: 50, end: 100}}},
+				{index: 1, neighbors: []intervalCase{{begin: 70, end: 200}}},
+			},
+		},
+		{
+			name: "same beginnings",
+			intervals: []intervalCase{
+				{begin: 50, end: 200},
+				{begin: 50, end: 401},
+				{begin: 50, end: 201},
+				{begin: 50, end: 302},
+			},
+			expNeighbors: []expNeighborCase{
+				{index: 0, neighbors: []intervalCase{{begin: 50, end: 201}, {begin: 50, end: 302}, {begin: 50, end: 401}}},
+				{index: 1, neighbors: []intervalCase{{begin: 50, end: 200}, {begin: 50, end: 201}, {begin: 50, end: 302}}},
+				{index: 2, neighbors: []intervalCase{{begin: 50, end: 200}, {begin: 50, end: 302}, {begin: 50, end: 401}}},
+				{index: 3, neighbors: []intervalCase{{begin: 50, end: 200}, {begin: 50, end: 201}, {begin: 50, end: 401}}},
+			},
+		},
+		{
+			name: "three intersecting",
+			intervals: []intervalCase{
+				{begin: 70, end: 200},
+				{begin: 71, end: 150},
+				{begin: 50, end: 100},
+			},
+			expNeighbors: []expNeighborCase{
+				{index: 0, neighbors: []intervalCase{{begin: 50, end: 100}, {begin: 71, end: 150}}},
+				{index: 1, neighbors: []intervalCase{{begin: 50, end: 100}, {begin: 70, end: 200}}},
+				{index: 2, neighbors: []intervalCase{{begin: 70, end: 200}, {begin: 71, end: 150}}},
+			},
+		},
+		{
+			name: "two enclosing interval",
+			intervals: []intervalCase{
+				{begin: 50, end: 100},
+				{begin: 25, end: 200},
+				{begin: 40, end: 1000},
+			},
+			expNeighbors: []expNeighborCase{
+				{index: 0, neighbors: []intervalCase{{begin: 25, end: 200}, {begin: 40, end: 1000}}},
+				{index: 1, neighbors: []intervalCase{{begin: 40, end: 1000}, {begin: 50, end: 100}}},
+				{index: 2, neighbors: []intervalCase{{begin: 25, end: 200}, {begin: 50, end: 100}}},
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			manager := newIntervalManager()
+			for i, inter := range tc.intervals {
+				n := &node{id: i, r: RealReg(1)}
+				manager.insert(n, inter.begin, inter.end)
+			}
+			manager.build()
+
+			for i, exp := range tc.expNeighbors {
+				it := tc.intervals[exp.index]
+				key := intervalTreeNodeKey(it.begin, it.end)
+
+				var found []intervalCase
+				for _, n := range manager.intervals[key].neighbors {
+					found = append(found, intervalCase{begin: n.begin, end: n.end})
+				}
+				sort.Slice(found, func(i, j int) bool {
+					return found[i].begin < found[j].begin
+				})
+				require.Equal(t, exp.neighbors, found, fmt.Sprintf("case=%d", i))
+			}
+		})
+	}
+}
+
+func TestIntervalManager_collectActiveNodes(t *testing.T) {
+	type (
+		queryCase struct {
+			query programCounter
+			exp   []int
+		}
+		intervalCase struct {
+			begin, end programCounter
+			id         int
+		}
+	)
+
+	newQueryCase := func(s programCounter, exp ...int) queryCase {
+		return queryCase{query: s, exp: exp}
+	}
+
+	for _, tc := range []struct {
+		name       string
+		intervals  []intervalCase
+		queryCases []queryCase
+	}{
+		{
+			name:      "single",
+			intervals: []intervalCase{{begin: 0, end: 100, id: 0}},
+			queryCases: []queryCase{
+				newQueryCase(0, 0),
+				newQueryCase(1, 0),
+				newQueryCase(101),
+			},
+		},
+		{
+			name:      "single/2",
+			intervals: []intervalCase{{begin: 50, end: 100, id: 0}},
+			queryCases: []queryCase{
+				newQueryCase(48),
+				newQueryCase(50, 0),
+				newQueryCase(51, 0),
+				newQueryCase(101),
+			},
+		},
+		{
+			name:      "same id for different intervals",
+			intervals: []intervalCase{{begin: 50, end: 100, id: 0xa}, {begin: 150, end: 200, id: 0xa}},
+			queryCases: []queryCase{
+				newQueryCase(0),
+				newQueryCase(50, 0xa),
+				newQueryCase(101),
+				newQueryCase(150, 0xa),
+			},
+		},
+		{
+			name:      "two disjoint intervals",
+			intervals: []intervalCase{{begin: 50, end: 100, id: 0xa}, {begin: 150, end: 200, id: 0xb}},
+			queryCases: []queryCase{
+				newQueryCase(0),
+				newQueryCase(50, 0xa),
+				newQueryCase(51, 0xa),
+				newQueryCase(101),
+				newQueryCase(150, 0xb),
+				newQueryCase(200, 0xb),
+				newQueryCase(201),
+			},
+		},
+		{
+			name:      "two intersecting intervals",
+			intervals: []intervalCase{{begin: 50, end: 100, id: 0xa}, {begin: 51, end: 200, id: 0xb}},
+			queryCases: []queryCase{
+				newQueryCase(0),
+				newQueryCase(1),
+				newQueryCase(49),
+				newQueryCase(50, 0xa),
+				newQueryCase(51, 0xa, 0xb),
+				newQueryCase(70, 0xa, 0xb),
+				newQueryCase(100, 0xa, 0xb),
+				newQueryCase(101, 0xb),
+				newQueryCase(1001),
+			},
+		},
+		{
+			name:      "two enclosing interval",
+			intervals: []intervalCase{{begin: 50, end: 100, id: 0xa}, {begin: 25, end: 200, id: 0xb}, {begin: 40, end: 1000, id: 0xc}},
+			queryCases: []queryCase{
+				newQueryCase(24),
+				newQueryCase(25, 0xb),
+				newQueryCase(39, 0xb),
+				newQueryCase(40, 0xb, 0xc),
+				newQueryCase(49, 0xb, 0xc),
+				newQueryCase(50, 0xa, 0xb, 0xc),
+				newQueryCase(51, 0xa, 0xb, 0xc),
+				newQueryCase(99, 0xa, 0xb, 0xc),
+				newQueryCase(100, 0xa, 0xb, 0xc),
+				newQueryCase(101, 0xb, 0xc),
+				newQueryCase(200, 0xb, 0xc),
+				newQueryCase(201, 0xc),
+				newQueryCase(1000, 0xc),
+				newQueryCase(1001),
+			},
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			for _, onlyReal := range []bool{false, true} {
+				t.Run(fmt.Sprintf("onlyReal=%t", onlyReal), func(t *testing.T) {
+					manager := newIntervalManager()
+					for _, inter := range tc.intervals {
+						n := &node{id: inter.id, r: RealReg(1)}
+						manager.insert(n, inter.begin, inter.end)
+						key := intervalTreeNodeKey(inter.begin, inter.end)
+						inserted := manager.intervals[key]
+
+						// They are ignored.
+						if onlyReal {
+							inserted.nodes = append(inserted.nodes, &node{v: VRegInvalid.SetRealReg(RealRegInvalid)}) // non-real reg should be ignored.
+						} else {
+							inserted.nodes = append(inserted.nodes, &node{v: FromRealReg(1, RegTypeInt)})
+							inserted.nodes = append(inserted.nodes, &node{v: FromRealReg(1, RegTypeFloat)})
+							inserted.nodes = append(inserted.nodes, &node{v: VReg(1)})
+						}
+					}
+					manager.build()
+					for _, qc := range tc.queryCases {
+						t.Run(fmt.Sprintf("%d", qc.query), func(t *testing.T) {
+							var collected []*node
+							manager.collectActiveNodes(qc.query, &collected, onlyReal)
+							require.Equal(t, len(qc.exp), len(collected))
+							var foundIDs []int
+							for _, n := range collected {
+								foundIDs = append(foundIDs, n.id)
+							}
+							sort.Slice(foundIDs, func(i, j int) bool {
+								return foundIDs[i] < foundIDs[j]
+							})
+							require.Equal(t, qc.exp, foundIDs)
+						})
+					}
+				})
+			}
+		})
+	}
+}
diff --git a/internal/engine/wazevo/backend/regalloc/regalloc_test.go b/internal/engine/wazevo/backend/regalloc/regalloc_test.go
index 2e51478a..9a6d13cb 100644
--- a/internal/engine/wazevo/backend/regalloc/regalloc_test.go
+++ b/internal/engine/wazevo/backend/regalloc/regalloc_test.go
@@ -445,10 +445,10 @@ func TestAllocator_livenessAnalysis(t *testing.T) {
 				actual := &a.blockInfos[blockID]
 				exp := tc.exp[blockID]
 				initMapInInfo(exp)
-				saved := actual.intervalTree
-				actual.intervalTree = nil // Don't compare intervalTree.
+				saved := actual.intervalMng
+				actual.intervalMng = nil // Don't compare intervalManager.
 				require.Equal(t, exp, actual, "\n[exp for block[%d]]\n%s\n[actual for block[%d]]\n%s", blockID, exp, blockID, actual)
-				actual.intervalTree = saved
+				actual.intervalMng = saved
 			}
 
 			// Sanity check: buildLiveRanges should not panic.