diff --git a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt index 720d69bf6bc..c8bad568c0f 100644 --- a/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt +++ b/kotlin-native/backend.native/compiler/ir/backend.native/src/org/jetbrains/kotlin/backend/konan/optimizations/EscapeAnalysis.kt @@ -6,6 +6,7 @@ package org.jetbrains.kotlin.backend.konan.optimizations import org.jetbrains.kotlin.backend.common.atMostOne +import org.jetbrains.kotlin.backend.common.peek import org.jetbrains.kotlin.backend.common.pop import org.jetbrains.kotlin.backend.common.push import org.jetbrains.kotlin.backend.konan.* @@ -143,6 +144,7 @@ internal object EscapeAnalysis { val ROOT_SCOPE = 0 val RETURN_VALUE = -1 val PARAMETER = -2 + val GLOBAL = -3 } object DivergenceResolutionParams { @@ -545,6 +547,12 @@ internal object EscapeAnalysis { } } + private enum class ComputationState { + NEW, + PENDING, + DONE + } + private class Stats { var globalAllocsCount = 0 var stackAllocsCount = 0 @@ -555,6 +563,14 @@ internal object EscapeAnalysis { private val stats = Stats() + private fun maxPointsToGraphSizeOf(function: DataFlowIR.FunctionSymbol) = with(DivergenceResolutionParams) { + // A heuristic: the majority of functions have their points-to graph size linear in number of IR (or DFG) nodes, + // there are exceptions but it's a trade-off we have to make. + // The trick with [NegligibleSize] handles functions that basically delegate their work to other functions. + val numberOfNodes = intraproceduralAnalysisResults[function]!!.function.body.allScopes.sumOf { it.nodes.size } + NegligibleSize + numberOfNodes * SwellingFactor + } + private fun analyze(callGraph: CallGraph, multiNode: DirectedGraphMultiNode) { val nodes = multiNode.nodes.filter { intraproceduralAnalysisResults.containsKey(it) }.toMutableSet() @@ -571,11 +587,12 @@ internal object EscapeAnalysis { } } - val pointsToGraphs = mutableMapOf() + var pointsToGraphs = mutableMapOf() + var failedToConverge = false val toAnalyze = mutableSetOf() toAnalyze.addAll(nodes) val numberOfRuns = nodes.associateWith { 0 }.toMutableMap() - while (toAnalyze.isNotEmpty()) { + while (!failedToConverge && toAnalyze.isNotEmpty()) { val function = toAnalyze.first() toAnalyze.remove(function) numberOfRuns[function] = numberOfRuns[function]!! + 1 @@ -587,44 +604,31 @@ internal object EscapeAnalysis { val pointsToGraph = PointsToGraph(function) pointsToGraphs[function] = pointsToGraph - val maxAllowedGraphSize = with(DivergenceResolutionParams) { - // A heuristic: the majority of functions have their points-to graph size linear in number of IR (or DFG) nodes, - // there are exceptions but it's a trade-off we have to make. - // The trick with [NegligibleSize] handles functions that basically delegate their work to other functions. - val numberOfNodes = intraproceduralAnalysisResults[function]!!.function.body.allScopes.sumOf { it.nodes.size } - NegligibleSize + numberOfNodes * SwellingFactor - } - - var analyzedSuccessfully = analyze(callGraph, pointsToGraph, function, maxAllowedGraphSize) - var processCallers = true - if (analyzedSuccessfully) { + if (!analyze(callGraph.directEdges[function]!!.callSites, pointsToGraph, function, maxPointsToGraphSizeOf(function))) { + failedToConverge = true + } else { val endResult = escapeAnalysisResults[function]!! if (startResult == endResult) { context.log { "Escape analysis is not changed" } - processCallers = false } else { context.log { "Escape analysis was refined:\n$endResult" } if (numberOfRuns[function]!! > DivergenceResolutionParams.MaxAttempts) - analyzedSuccessfully = false + failedToConverge = true + else { + callGraph.reversedEdges[function]?.forEach { + if (nodes.contains(it)) + toAnalyze.add(it) + } + } } } - if (!analyzedSuccessfully) { - // TODO: suboptimal. May be it is possible somehow handle the entire component at once? - context.log { - "WARNING: Escape analysis for $function seems not to be converging." + - " Assuming conservative results." - } - escapeAnalysisResults[function] = FunctionEscapeAnalysisResult.pessimistic(function.parameters.size) - nodes.remove(function) - } + if (failedToConverge) + context.log { "WARNING: Escape analysis for $function seems not to be converging. Falling back to conservative strategy." } + } - if (processCallers) { - callGraph.reversedEdges[function]?.forEach { - if (nodes.contains(it)) - toAnalyze.add(it) - } - } + if (failedToConverge) { + pointsToGraphs = analyzeComponentPessimistically(callGraph, multiNode) } pointsToGraphs.forEach { (function, graph) -> @@ -634,8 +638,6 @@ internal object EscapeAnalysis { stats.totalPTGSize += graph.allNodes.size stats.totalDFGSize += intraproceduralAnalysisResults[function]!!.function.body.allScopes.sumOf { it.nodes.size } - // TODO: suboptimal. - if (function !in nodes) return@forEach for (node in graph.nodes.keys) { node.ir?.let { val lifetime = graph.lifetimeOf(node) @@ -653,6 +655,73 @@ internal object EscapeAnalysis { } } + private fun analyzeComponentPessimistically( + callGraph: CallGraph, + multiNode: DirectedGraphMultiNode + ): MutableMap { + val nodes = multiNode.nodes.filter { intraproceduralAnalysisResults.containsKey(it) } + val pointsToGraphs = mutableMapOf() + val computationStates = mutableMapOf() + nodes.forEach { computationStates[it] = ComputationState.NEW } + val toAnalyze = nodes.toMutableList() + while (toAnalyze.isNotEmpty()) { + val function = toAnalyze.peek()!! + val state = computationStates[function]!! + val callSites = callGraph.directEdges[function]!!.callSites + when (state) { + ComputationState.NEW -> { + computationStates[function] = ComputationState.PENDING + for (callSite in callSites) { + val callee = callSite.actualCallee + val calleeComputationState = computationStates[callee] + if (callSite.isVirtual + || callee !is DataFlowIR.FunctionSymbol.Declared // An external call. + || calleeComputationState == null // A call to a function from other component. + || calleeComputationState == ComputationState.DONE // Already analyzed. + ) { + continue + } + + if (calleeComputationState == ComputationState.PENDING) { + // A cycle - break it by assuming nothing about the callee. + // This is not the callee's final result - it will be recomputed later in the loop. + escapeAnalysisResults[callee] = FunctionEscapeAnalysisResult.pessimistic(callee.parameters.size) + } else { + computationStates[callee] = ComputationState.NEW + toAnalyze.push(callee) + } + } + } + + ComputationState.PENDING -> { + toAnalyze.pop() + computationStates[function] = ComputationState.DONE + val pointsToGraph = PointsToGraph(function) + if (analyze(callSites, pointsToGraph, function, maxPointsToGraphSizeOf(function))) + pointsToGraphs[function] = pointsToGraph + else { + // TODO: suboptimal. May be it is possible somehow handle the entire component at once? + context.log { + "WARNING: Escape analysis for $function seems not to be converging." + + " Assuming conservative results." + } + escapeAnalysisResults[function] = FunctionEscapeAnalysisResult.pessimistic(function.parameters.size) + // Invalidate the points-to graph. + pointsToGraphs[function] = PointsToGraph(function).apply { + allNodes.forEach { it.depth = Depths.GLOBAL } + } + } + } + + ComputationState.DONE -> { + toAnalyze.pop() + } + } + } + + return pointsToGraphs + } + private fun arrayLengthOf(node: DataFlowIR.Node): Int? = (node as? DataFlowIR.Node.SimpleConst<*>)?.value as? Int // In case of several possible values, it's unknown what is used. @@ -679,7 +748,7 @@ internal object EscapeAnalysis { pointerSize /* typeinfo */ + 4 /* size */ + itemSize * length.toLong() private fun analyze( - callGraph: CallGraph, + callSites: List, pointsToGraph: PointsToGraph, function: DataFlowIR.FunctionSymbol.Declared, maxAllowedGraphSize: Int @@ -688,7 +757,7 @@ internal object EscapeAnalysis { pointsToGraph.log() pointsToGraph.logDigraph(false) - callGraph.directEdges[function]!!.callSites.forEach { + callSites.forEach { val callee = it.actualCallee val calleeEAResult = if (it.isVirtual) getExternalFunctionEAResult(it) @@ -757,7 +826,8 @@ internal object EscapeAnalysis { STACK, LOCAL, PARAMETER, - RETURN_VALUE + RETURN_VALUE, + GLOBAL } private sealed class PointsToGraphEdge(val node: PointsToGraphNode) { @@ -787,6 +857,7 @@ internal object EscapeAnalysis { } val kind get() = when { + depth == Depths.GLOBAL -> PointsToGraphNodeKind.GLOBAL depth == Depths.PARAMETER -> PointsToGraphNodeKind.PARAMETER depth == Depths.RETURN_VALUE -> PointsToGraphNodeKind.RETURN_VALUE depth != nodeInfo.depth -> PointsToGraphNodeKind.LOCAL @@ -858,6 +929,8 @@ internal object EscapeAnalysis { if (escapes(node)) Lifetime.GLOBAL else when (node.kind) { + PointsToGraphNodeKind.GLOBAL -> Lifetime.GLOBAL + PointsToGraphNodeKind.PARAMETER -> Lifetime.ARGUMENT PointsToGraphNodeKind.STACK -> {