From f2fba79ae832fe29f6bb49cdffce17424c92ccec Mon Sep 17 00:00:00 2001 From: jonathanvdc Date: Sat, 8 Nov 2025 23:06:52 -0500 Subject: [PATCH 1/6] Refactor ENode equality and hash code methods for improved performance --- .../main/scala/foresight/eqsat/ENode.scala | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/foresight/src/main/scala/foresight/eqsat/ENode.scala b/foresight/src/main/scala/foresight/eqsat/ENode.scala index fec89df2..fe72c865 100644 --- a/foresight/src/main/scala/foresight/eqsat/ENode.scala +++ b/foresight/src/main/scala/foresight/eqsat/ENode.scala @@ -4,7 +4,6 @@ import foresight.eqsat.collections.{SlotMap, SlotSeq, SlotSet} import foresight.util.Debug import foresight.util.collections.UnsafeSeqFromArray -import java.util.concurrent.atomic.AtomicInteger import scala.collection.compat.immutable.ArraySeq /** @@ -28,8 +27,8 @@ final class ENode[+NodeT] private ( private val _uses: Array[Slot], private val _args: Array[EClassCall] ) extends Node[NodeT, EClassCall] with ENodeSymbol[NodeT] { - // Cached hash code to make hashing and equality fast - private val _hash: AtomicInteger = new AtomicInteger(0) + // Cached hash code to make hashing and equality fast (benign data race; like String.hash) + private var _hash: Int = 0 /** * Slots introduced by this node that are scoped locally and invisible to parents. These are @@ -294,13 +293,36 @@ final class ENode[+NodeT] private ( // --- case-class-like API preservation --- override def toString: String = s"ENode($nodeType, $definitions, $uses, $args)" + @inline + private def callsEqualFast(a: Array[EClassCall], b: Array[EClassCall]): Boolean = { + if (a eq b) return true + if (a.length != b.length) return false + var i = 0 + while (i < a.length) { + val ai = a(i); val bi = b(i) + // Compare the e-class ids first (cheap) + if (ai.ref.id != bi.ref.id) return false + // Then compare the SlotMap by reference first, fall back to equals only if needed + val aArgs = ai.args; val bArgs = bi.args + if ((aArgs ne bArgs) && !(aArgs == bArgs)) return false + i += 1 + } + true + } + //noinspection ComparingUnrelatedTypes override def equals(other: Any): Boolean = other match { case that: ENode[_] => - this.nodeType == that.nodeType && - ENode.arraysEqual(this._definitions, that._definitions) && - ENode.arraysEqual(this._uses, that._uses) && - ENode.arraysEqual(this._args, that._args) + (this eq that) || ( + this.nodeType == that.nodeType && + // quick length checks to short-circuit before scanning arrays + this._definitions.length == that._definitions.length && + this._uses.length == that._uses.length && + this._args.length == that._args.length && + ENode.arraysEqual(this._definitions, that._definitions) && + ENode.arraysEqual(this._uses, that._uses) && + callsEqualFast(this._args, that._args) + ) case _ => false } @@ -330,13 +352,12 @@ final class ENode[+NodeT] private ( h } - override def hashCode(): Int = { - val cached = _hash.get() + @inline override def hashCode(): Int = { + val cached = _hash if (cached != 0) return cached - val h = computeHash() val result = if (h == 0) 1 else h - _hash.compareAndSet(0, result) + _hash = result result } From 4182f824fcc279055cb62a709b91ea19cbdcafce Mon Sep 17 00:00:00 2001 From: jonathanvdc Date: Sat, 8 Nov 2025 23:39:29 -0500 Subject: [PATCH 2/6] Optimize hash consing and equality checks for ENode with fastutil collections --- .../main/scala/foresight/eqsat/ENode.scala | 79 ++++++++++++++----- .../AbstractMutableHashConsEGraph.scala | 4 +- .../hashCons/mutable/HashConsEGraph.scala | 65 ++++++++++----- project/Dependencies.scala | 3 +- 4 files changed, 107 insertions(+), 44 deletions(-) diff --git a/foresight/src/main/scala/foresight/eqsat/ENode.scala b/foresight/src/main/scala/foresight/eqsat/ENode.scala index fe72c865..fec0fdfe 100644 --- a/foresight/src/main/scala/foresight/eqsat/ENode.scala +++ b/foresight/src/main/scala/foresight/eqsat/ENode.scala @@ -310,46 +310,85 @@ final class ENode[+NodeT] private ( true } + @inline + private def slotsEqualByRef(a: Array[Slot], b: Array[Slot]): Boolean = { + if (a eq b) return true + if (a.length != b.length) return false + var i = 0 + while (i < a.length) { + if (a(i) ne b(i)) return false + i += 1 + } + true + } + //noinspection ComparingUnrelatedTypes override def equals(other: Any): Boolean = other match { case that: ENode[_] => - (this eq that) || ( - this.nodeType == that.nodeType && - // quick length checks to short-circuit before scanning arrays - this._definitions.length == that._definitions.length && - this._uses.length == that._uses.length && - this._args.length == that._args.length && - ENode.arraysEqual(this._definitions, that._definitions) && - ENode.arraysEqual(this._uses, that._uses) && - callsEqualFast(this._args, that._args) - ) + if (this eq that) return true + // Cheap hash pre-check to avoid deep scans during collision probes + if (this.hashCode() != that.hashCode()) return false + // Now structural checks + (this.nodeType == that.nodeType) && + (this._definitions.length == that._definitions.length) && + (this._uses.length == that._uses.length) && + (this._args.length == that._args.length) && + slotsEqualByRef(this._definitions, that._definitions) && + slotsEqualByRef(this._uses, that._uses) && + callsEqualFast(this._args, that._args) case _ => false } - private def computeHash(): Int = { - var h = 1 - h = 31 * h + (if (nodeType == null) 0 else nodeType.hashCode) + @inline private def mix(h: Int, data: Int): Int = { + var k = data + k *= 0xcc9e2d51 + k = (k << 15) | (k >>> 17) + k *= 0x1b873593 + var res = h ^ k + res = (res << 13) | (res >>> 19) + res = res * 5 + 0xe6546b64 + res + } + @inline private def avalanche(h: Int, len: Int): Int = { + var x = h ^ len + x ^= (x >>> 16) + x *= 0x85ebca6b + x ^= (x >>> 13) + x *= 0xc2b2ae35 + x ^= (x >>> 16) + x + } + private def computeHash(): Int = { + // Murmur3-style mix for better avalanche; keep consistent with equals: + // - Slots are compared by reference => use identityHashCode for slots + // - Args compare ref.id and SlotMap by (ref or equals) => use id + identityHashCode(map) as primary signal + var h = 0 + val nt = if (nodeType == null) 0 else nodeType.hashCode + h = mix(h, nt) + // definitions (by reference) var i = 0 while (i < _definitions.length) { - h = 31 * h + _definitions(i).hashCode() + h = mix(h, System.identityHashCode(_definitions(i))) i += 1 } - + // uses (by reference) i = 0 while (i < _uses.length) { - h = 31 * h + _uses(i).hashCode() + h = mix(h, System.identityHashCode(_uses(i))) i += 1 } - + // args: mix ref id and structure of the SlotMap; include size to separate small maps i = 0 while (i < _args.length) { val arg = _args(i) - h = 31 * h + arg.ref.id - h = 31 * h + arg.args.hashCode() + h = mix(h, arg.ref.id) + // Use structural hash for SlotMap to remain consistent with equals (which may consider distinct instances equal) + h = mix(h, arg.args.hashCode()) i += 1 } - h + // length salt to distinguish permutations/shapes with same prefix + avalanche(h, 1 + _definitions.length + _uses.length + (_args.length << 1)) } @inline override def hashCode(): Int = { diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala index 453c8b5c..5e7e0566 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala @@ -118,10 +118,10 @@ private[hashCons] abstract class AbstractMutableHashConsEGraph[NodeT] } /** - * Query the hash cons for the given node. Returns null if the node is not in the hash cons. + * Query the hash cons for the given node. Returns an invalid e-class reference if the node is not in the hash cons. * * @param node The node to query. - * @return The e-class reference of the node, or null if the node is not in the hash cons. + * @return The e-class reference of the node, or an invalid reference if the node is not in the hash cons. */ private def nodeToClassOrNull(node: ENode[NodeT]): EClassRef = { nodeToRefOrElse(node, EClassRef.Invalid) diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala index 5f42f551..e67ad78a 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala @@ -1,58 +1,75 @@ package foresight.eqsat.hashCons.mutable import foresight.eqsat.collections.{SlotMap, SlotSet} -import foresight.eqsat.{EClassCall, EClassRef, ENode, ShapeCall} +import foresight.eqsat.{EClassRef, ENode} import foresight.eqsat.hashCons.{AbstractMutableHashConsEGraph, PermutationGroup} import foresight.util.Debug -import scala.collection.mutable +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap +import it.unimi.dsi.fastutil.ints.{Int2ObjectOpenHashMap, IntArrayList} private[eqsat] final class HashConsEGraph[NodeT] extends AbstractMutableHashConsEGraph[NodeT] { type ClassData = MutableEClassData[NodeT] type UnionFind = SlottedUnionFind protected override val unionFind: SlottedUnionFind = new SlottedUnionFind() - private val hashCons: mutable.HashMap[ENode[NodeT], EClassRef] = mutable.HashMap.empty - private val classData: mutable.HashMap[EClassRef, MutableEClassData[NodeT]] = mutable.HashMap.empty + private val hashCons = new Object2IntOpenHashMap[ENode[NodeT]]() + private val classData = new Int2ObjectOpenHashMap[MutableEClassData[NodeT]]() + hashCons.defaultReturnValue(EClassRef.Invalid.id) protected override def updateClassPermutations(ref: EClassRef, permutations: PermutationGroup[SlotMap]): Unit = { - val data = classData(ref) + val data = classData.get(ref.id) data.setPermutations(permutations) } protected override def updateClassSlotsAndPermutations(ref: EClassRef, slots: SlotSet, permutations: PermutationGroup[SlotMap]): Unit = { - val data = classData(ref) + val data = classData.get(ref.id) data.setSlots(slots) data.setPermutations(permutations) } - override def dataForClass(ref: EClassRef): MutableEClassData[NodeT] = classData(ref) - override def classes: Iterable[EClassRef] = classData.keys - protected override def shapes: Iterable[ENode[NodeT]] = hashCons.keys - override def nodeToRefOrElse(node: ENode[NodeT], default: => EClassRef): EClassRef = hashCons.getOrElse(node, default) + override def dataForClass(ref: EClassRef): MutableEClassData[NodeT] = classData.get(ref.id) + override def classes: Iterable[EClassRef] = new Iterable[EClassRef] { + override def iterator: Iterator[EClassRef] = new Iterator[EClassRef] { + private val it = classData.keySet().iterator() + override def hasNext: Boolean = it.hasNext + override def next(): EClassRef = EClassRef(it.nextInt()) + } + } + + protected override def shapes: Iterable[ENode[NodeT]] = new Iterable[ENode[NodeT]] { + override def iterator: Iterator[ENode[NodeT]] = new Iterator[ENode[NodeT]] { + private val it = hashCons.keySet().iterator() + override def hasNext: Boolean = it.hasNext + override def next(): ENode[NodeT] = it.next() + } + } + + override def nodeToRefOrElse(node: ENode[NodeT], default: => EClassRef): EClassRef = { + val id = hashCons.getInt(node) + if (id != EClassRef.Invalid.id) EClassRef(id) else default + } protected override def createEmptyClass(slots: SlotSet): EClassRef = { val ref = unionFind.add(slots) - val data = new MutableEClassData[NodeT](slots, PermutationGroup.identity(SlotMap.identity(slots))) - classData.put(ref, data) - + classData.put(ref.id, data) ref } protected override def addNodeToClass(ref: EClassRef, shape: ENode[NodeT], renaming: SlotMap): Unit = { // Set the node in the hash cons, update the class data and add the node to the argument e-classes' users. - val data = classData(ref) - hashCons.put(shape, ref) + val data = classData.get(ref.id) + hashCons.put(shape, ref.id) data.addNode(shape, renaming) val argsArray = shape.unsafeArgsArray var i = 0 while (i < argsArray.length) { val arg = argsArray(i) - val argData = classData(arg.ref) + val argData = classData.get(arg.ref.id) argData.addUser(shape) i += 1 } @@ -70,15 +87,15 @@ private[eqsat] final class HashConsEGraph[NodeT] extends AbstractMutableHashCons assert(shape.isShape) } - val data = classData(ref) - hashCons.remove(shape) + val data = classData.get(ref.id) + hashCons.removeInt(shape) data.removeNode(shape) val argsArray = shape.unsafeArgsArray var i = 0 while (i < argsArray.length) { val arg = argsArray(i) - val argData = classData(arg.ref) + val argData = classData.get(arg.ref.id) argData.removeUser(shape) i += 1 } @@ -88,8 +105,14 @@ private[eqsat] final class HashConsEGraph[NodeT] extends AbstractMutableHashCons * Unlinks all empty e-classes from the class data map. An e-class is considered empty if it has no nodes. */ protected override def unlinkEmptyClasses(): Unit = { - val emptyClasses = classData.collect { case (ref, data) if data.nodes.isEmpty => ref } - emptyClasses.foreach(ref => classData.remove(ref)) + val it = classData.int2ObjectEntrySet().fastIterator() + val toRemove = new IntArrayList() + while (it.hasNext) { + val e = it.next() + if (e.getValue.nodes.isEmpty) toRemove.add(e.getIntKey) + } + val rit = toRemove.iterator() + while (rit.hasNext) classData.remove(rit.nextInt()) } override def emptied: this.type = { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 98f1aea4..ec7987a9 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -17,6 +17,7 @@ object Dependencies { val baseDeps = Seq( "org.scala-lang.modules" %% "scala-xml" % scalaXmlVersion ) + val fastutil = Seq("it.unimi.dsi" % "fastutil" % "8.5.13") val collectionCompat = Seq("org.scala-lang.modules" %% "scala-collection-compat" % "2.13.0") @@ -33,7 +34,7 @@ object Dependencies { Seq("org.scala-lang.modules" %% "scala-parallel-collections" % "1.0.4") else Nil - baseDeps ++ collectionCompat ++ reflectDeps ++ parallelCollections + baseDeps ++ collectionCompat ++ reflectDeps ++ parallelCollections ++ fastutil } def testDependencies(scalaVersion: String): Seq[ModuleID] = { From 47bd6ee3b703ea916af9abe8811c1381b706b9e6 Mon Sep 17 00:00:00 2001 From: jonathanvdc Date: Sat, 8 Nov 2025 23:43:40 -0500 Subject: [PATCH 3/6] Optimize hash consing performance by adjusting load factor in HashConsEGraph --- .../foresight/eqsat/hashCons/mutable/HashConsEGraph.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala index e67ad78a..f01a4ed2 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala @@ -4,7 +4,7 @@ import foresight.eqsat.collections.{SlotMap, SlotSet} import foresight.eqsat.{EClassRef, ENode} import foresight.eqsat.hashCons.{AbstractMutableHashConsEGraph, PermutationGroup} import foresight.util.Debug - +import it.unimi.dsi.fastutil.Hash import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap import it.unimi.dsi.fastutil.ints.{Int2ObjectOpenHashMap, IntArrayList} @@ -13,7 +13,7 @@ private[eqsat] final class HashConsEGraph[NodeT] extends AbstractMutableHashCons type UnionFind = SlottedUnionFind protected override val unionFind: SlottedUnionFind = new SlottedUnionFind() - private val hashCons = new Object2IntOpenHashMap[ENode[NodeT]]() + private val hashCons = new Object2IntOpenHashMap[ENode[NodeT]](16, Hash.VERY_FAST_LOAD_FACTOR) private val classData = new Int2ObjectOpenHashMap[MutableEClassData[NodeT]]() hashCons.defaultReturnValue(EClassRef.Invalid.id) From 65dc86e18dea9e797147576f0f75c6a6ec5d50e1 Mon Sep 17 00:00:00 2001 From: jonathanvdc Date: Sat, 8 Nov 2025 23:55:27 -0500 Subject: [PATCH 4/6] Refactor nodeToRefOrInvalid method names --- .../AbstractMutableHashConsEGraph.scala | 18 ++++-------------- .../hashCons/ReadOnlyHashConsEGraph.scala | 18 +++++++++++++++--- .../hashCons/immutable/HashConsEGraph.scala | 4 ++-- .../immutable/HashConsEGraphBuilder.scala | 4 ++-- .../hashCons/mutable/HashConsEGraph.scala | 4 ++-- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala index 5e7e0566..00c734b5 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/AbstractMutableHashConsEGraph.scala @@ -117,16 +117,6 @@ private[hashCons] abstract class AbstractMutableHashConsEGraph[NodeT] } } - /** - * Query the hash cons for the given node. Returns an invalid e-class reference if the node is not in the hash cons. - * - * @param node The node to query. - * @return The e-class reference of the node, or an invalid reference if the node is not in the hash cons. - */ - private def nodeToClassOrNull(node: ENode[NodeT]): EClassRef = { - nodeToRefOrElse(node, EClassRef.Invalid) - } - private def slots(ref: EClassRef): SlotSet = dataForClass(ref).slots /** @@ -393,7 +383,7 @@ private[hashCons] abstract class AbstractMutableHashConsEGraph[NodeT] * @param node The node to repair. */ def repairNodeWithoutSlots(node: ENode[NodeT]): Unit = { - val ref = nodeToClassOrNull(node) + val ref = nodeToRefOrInvalid(node) if (Debug.isEnabled) { assert(ref != EClassRef.Invalid, "The node to repair must be in the hash-cons.") assert(!node.hasSlots, "The node to repair must not have slots.") @@ -411,7 +401,7 @@ private[hashCons] abstract class AbstractMutableHashConsEGraph[NodeT] val canonicalNode = canonicalizeWithoutSlots(node) if (canonicalNode != node) { - nodeToClassOrNull(canonicalNode) match { + nodeToRefOrInvalid(canonicalNode) match { case EClassRef.Invalid => // Eliminate the old node from the e-class and add the canonicalized node. removeNodeFromClass(ref, node) @@ -448,7 +438,7 @@ private[hashCons] abstract class AbstractMutableHashConsEGraph[NodeT] // 3. The canonicalized node is different from the original node, and the canonicalized node is not in the // hash-cons map. In this case, we add the canonicalized node to the hash-cons and queue its arguments // for parent set repair. - val ref = nodeToClassOrNull(node) + val ref = nodeToRefOrInvalid(node) if (Debug.isEnabled) { assert(ref != EClassRef.Invalid, "The node to repair must be in the hash-cons.") } @@ -488,7 +478,7 @@ private[hashCons] abstract class AbstractMutableHashConsEGraph[NodeT] } if (canonicalNode.shape != node) { - nodeToClassOrNull(canonicalNode.shape) match { + nodeToRefOrInvalid(canonicalNode.shape) match { case EClassRef.Invalid => // Eliminate the old node from the e-class and add the canonicalized node. removeNodeFromClass(ref, node) diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/ReadOnlyHashConsEGraph.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/ReadOnlyHashConsEGraph.scala index 83424399..fdfbac1e 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/ReadOnlyHashConsEGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/ReadOnlyHashConsEGraph.scala @@ -30,6 +30,15 @@ private[hashCons] trait ReadOnlyHashConsEGraph[NodeT] extends EGraph[NodeT] { */ protected val unionFind: UnionFind + /** + * Retrieves the e-class reference for a given e-node, or returns an invalid reference if the e-node is not found. + * This method does not canonicalize the e-node before looking it up; it assumes the caller has already done so + * and simply performs a hash cons lookup. + * @param node The e-node to look up. + * @return The e-class reference corresponding to the e-node, or an invalid reference if not found. + */ + def nodeToRefOrInvalid(node: ENode[NodeT]): EClassRef + /** * Retrieves the e-class reference for a given e-node, or returns a default value if the e-node is not found. * This method does not canonicalize the e-node before looking it up; it assumes the caller has already done so @@ -38,7 +47,10 @@ private[hashCons] trait ReadOnlyHashConsEGraph[NodeT] extends EGraph[NodeT] { * @param default A default e-class reference to return if the e-node is not found. * @return The e-class reference corresponding to the e-node, or the default value if not found. */ - def nodeToRefOrElse(node: ENode[NodeT], default: => EClassRef): EClassRef + final def nodeToRefOrElse(node: ENode[NodeT], default: => EClassRef): EClassRef = { + val ref = nodeToRefOrInvalid(node) + if (ref.isInvalid) default else ref + } /** * Retrieves the data associated with a given e-class. Assumes that the e-class reference is canonical. @@ -165,7 +177,7 @@ private[hashCons] trait ReadOnlyHashConsEGraph[NodeT] extends EGraph[NodeT] { assert(renamedShape.shape.isShape) } - val ref = nodeToRefOrElse(renamedShape.shape, EClassRef.Invalid) + val ref = nodeToRefOrInvalid(renamedShape.shape) if (ref.isInvalid) { return null } @@ -185,7 +197,7 @@ private[hashCons] trait ReadOnlyHashConsEGraph[NodeT] extends EGraph[NodeT] { assert(!node.hasSlots) } - val ref = nodeToRefOrElse(node, EClassRef.Invalid) + val ref = nodeToRefOrInvalid(node) if (ref.isInvalid) { return null } diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraph.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraph.scala index a0a0b625..b9785940 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraph.scala @@ -46,8 +46,8 @@ private[eqsat] final case class HashConsEGraph[NodeT] private[hashCons](protecte unionFind.findOrNull(ref) } - override def nodeToRefOrElse(node: ENode[NodeT], default: => EClassRef): EClassRef = { - hashCons.getOrElse(node, default) + override def nodeToRefOrInvalid(node: ENode[NodeT]): EClassRef = { + hashCons.getOrElse(node, EClassRef.Invalid) } override def dataForClass(ref: EClassRef): EClassData[NodeT] = { diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraphBuilder.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraphBuilder.scala index 96ef316e..eb8ad13b 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraphBuilder.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/immutable/HashConsEGraphBuilder.scala @@ -21,8 +21,8 @@ private final class HashConsEGraphBuilder[NodeT](protected override val unionFin protected override def shapes: Iterable[ENode[NodeT]] = hashCons.keys - override def nodeToRefOrElse(node: ENode[NodeT], default: => EClassRef): EClassRef = { - hashCons.getOrElse(node, default) + override def nodeToRefOrInvalid(node: ENode[NodeT]): EClassRef = { + hashCons.getOrElse(node, EClassRef.Invalid) } override def dataForClass(ref: EClassRef): EClassData[NodeT] = { diff --git a/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala b/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala index f01a4ed2..198ac586 100644 --- a/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala +++ b/foresight/src/main/scala/foresight/eqsat/hashCons/mutable/HashConsEGraph.scala @@ -47,9 +47,9 @@ private[eqsat] final class HashConsEGraph[NodeT] extends AbstractMutableHashCons } } - override def nodeToRefOrElse(node: ENode[NodeT], default: => EClassRef): EClassRef = { + override def nodeToRefOrInvalid(node: ENode[NodeT]): EClassRef = { val id = hashCons.getInt(node) - if (id != EClassRef.Invalid.id) EClassRef(id) else default + EClassRef(id) } protected override def createEmptyClass(slots: SlotSet): EClassRef = { From 15bdff39549afbdb51b887ecace01ac9815184e4 Mon Sep 17 00:00:00 2001 From: jonathanvdc Date: Sun, 9 Nov 2025 00:04:21 -0500 Subject: [PATCH 5/6] Precompute reification map size in CommandSchedule --- .../main/scala/foresight/eqsat/commands/CommandSchedule.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala b/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala index 6f2e25a7..1658f62e 100644 --- a/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala +++ b/foresight/src/main/scala/foresight/eqsat/commands/CommandSchedule.scala @@ -97,7 +97,8 @@ final case class CommandSchedule[NodeT](batchZero: (ArraySeq[EClassSymbol.Virtua def apply(egraph: mutable.EGraph[NodeT], parallelize: ParallelMap): Boolean = { - val reification = new util.IdentityHashMap[EClassSymbol.Virtual, foresight.eqsat.EClassCall]() + val reificationMapEntries = batchZero._1.length + otherBatches.map(_._1.length).sum + val reification = new util.IdentityHashMap[EClassSymbol.Virtual, foresight.eqsat.EClassCall](reificationMapEntries) var anyChanges: Boolean = false anyChanges = anyChanges | applyBatchZero(egraph, parallelize, reification) From e301497fc275a526cb71527557b29652c8b91557 Mon Sep 17 00:00:00 2001 From: jonathanvdc Date: Sun, 9 Nov 2025 00:10:03 -0500 Subject: [PATCH 6/6] Optimize boundSlots method to avoid unnecessary array allocation for empty cases --- .../eqsat/rewriting/patterns/MutableMachineState.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/foresight/src/main/scala/foresight/eqsat/rewriting/patterns/MutableMachineState.scala b/foresight/src/main/scala/foresight/eqsat/rewriting/patterns/MutableMachineState.scala index 7a523c3b..59e27379 100644 --- a/foresight/src/main/scala/foresight/eqsat/rewriting/patterns/MutableMachineState.scala +++ b/foresight/src/main/scala/foresight/eqsat/rewriting/patterns/MutableMachineState.scala @@ -175,7 +175,12 @@ final class MutableMachineState[NodeT] private(val effects: Instruction.Effects, } private def boundSlots: ArrayMap[Slot, Slot] = { - ArrayMap.unsafeWrapArrays(effects.boundSlots.unsafeArray, java.util.Arrays.copyOf(boundSlotsArr, slotIdx), slotIdx) + if (slotIdx == 0) { + // Avoid allocating an empty array copy + ArrayMap.empty[Slot, Slot] + } else { + ArrayMap.unsafeWrapArrays(effects.boundSlots.unsafeArray, java.util.Arrays.copyOf(boundSlotsArr, slotIdx), slotIdx) + } } /** Convert to an immutable MachineState snapshot. */