[CFG] Improve the performance of control-flow info calculation
By assuming the control-flow graph visitor is pure, the change status of
each node can be tracked to only recalculate info as needed. This avoids
recalculating the data for a node, which could improve performance when
dealing with large graphs.
^KT-59670
diff --git a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/util/CfgTraverser.kt b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/util/CfgTraverser.kt
index ee8c192..d4128f2 100644
--- a/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/util/CfgTraverser.kt
+++ b/compiler/fir/checkers/src/org/jetbrains/kotlin/fir/analysis/cfa/util/CfgTraverser.kt
@@ -17,33 +17,56 @@
visitor: PathAwareControlFlowGraphVisitor<K, V>,
): Map<CFGNode<*>, PathAwareControlFlowInfo<K, V>> {
val nodeMap = LinkedHashMap<CFGNode<*>, PathAwareControlFlowInfo<K, V>>()
- while (traverseOnce(visitor, nodeMap)) {
- // had changes, continue
- }
+ val changed = hashSetOf<CFGNode<*>>()
+
+ // Before the first pass, the changed set will be empty.
+ // After the first pass, the changed set will be full.
+ // Once the changed set is empty, the traversal is stable.
+ do {
+ traverseOnce(visitor, nodeMap, changed)
+ } while (changed.isNotEmpty())
+
return nodeMap
}
private fun <K : Any, V : Any> ControlFlowGraph.traverseOnce(
visitor: PathAwareControlFlowGraphVisitor<K, V>,
nodeMap: MutableMap<CFGNode<*>, PathAwareControlFlowInfo<K, V>>,
-): Boolean {
- var changed = false
+ changed: MutableSet<CFGNode<*>>,
+ previousNodesCache: MutableMap<CFGNode<*>, List<CFGNode<*>>> = hashMapOf(),
+) {
for (node in nodes) {
- // TODO, KT-59670: if data for previousNodes hasn't changed, then should be no need to recompute data for this one
- val previousData = node.previousCfgNodes.mapNotNull { source ->
- nodeMap[source]?.let {
- visitor.visitEdge(source, node, node.edgeFrom(source), it)
+ changed.remove(node)
+
+ val previousNodes = previousNodesCache.getOrPut(node) { node.previousCfgNodes }
+ if (node !in nodeMap || previousNodes.any { it in changed }) {
+ var previousData: PathAwareControlFlowInfo<K, V>? = null
+ for (previousNode in previousNodes) {
+ // TODO(KT-59670) no reason to recalculate the edge if the node data hasn't changed.
+ val nodeData = nodeMap[previousNode] ?: continue
+ val edgeData = visitor.visitEdge(
+ from = previousNode,
+ to = node,
+ data = nodeData,
+ metadata = node.edgeFrom(previousNode),
+ )
+
+ previousData = when (previousData) {
+ null -> edgeData
+ else -> visitor.mergeInfo(previousData, edgeData, node)
+ }
}
- }.reduceOrNull { a, b -> visitor.mergeInfo(a, b, node) }
- val newData = node.accept(visitor, previousData ?: emptyNormalPathInfo())
- if (newData != nodeMap.put(node, newData)) {
- changed = true
+
+ val newData = node.accept(visitor, previousData ?: emptyNormalPathInfo())
+ if (newData != nodeMap.put(node, newData)) {
+ changed.add(node)
+ }
}
+
if (node is CFGNodeWithSubgraphs<*>) {
node.subGraphs.forEach {
- changed = changed or (visitor.visitSubGraph(node, it) && it.traverseOnce(visitor, nodeMap))
+ if (visitor.visitSubGraph(node, it)) it.traverseOnce(visitor, nodeMap, changed, previousNodesCache)
}
}
}
- return changed
}