From 2353414959f5a0869a0d07c3d5ef8c988a9a2bed Mon Sep 17 00:00:00 2001 From: Igor Chevdar Date: Mon, 31 Oct 2022 14:15:52 +0200 Subject: [PATCH] [K/N][optmz] Another way of resolving escape analysis divergence The idea is the following: if the analysis for some strongly connected component seems not to be converging, reanalyze it again considering all backward edges (i.e. leading to cycles) pessimistic, basically treating the corresponding calls as external ones. This helps to have more reasonable results for not-converging components still leaving the chance for them to converge optimistically first. --- .../konan/optimizations/EscapeAnalysis.kt | 143 +++++++++++++----- 1 file changed, 108 insertions(+), 35 deletions(-) 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 -> {