From 4e661a63373fbf25ca764af08f43835301a215b4 Mon Sep 17 00:00:00 2001 From: Igor Chevdar Date: Mon, 3 Apr 2023 21:22:39 +0300 Subject: [PATCH] [IR] Liveness analysis Compute actually used variables for specified IR nodes --- .../lower/optimizations/LivenessAnalysis.kt | 233 ++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/optimizations/LivenessAnalysis.kt diff --git a/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/optimizations/LivenessAnalysis.kt b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/optimizations/LivenessAnalysis.kt new file mode 100644 index 00000000000..d1b7f2043c0 --- /dev/null +++ b/compiler/ir/backend.common/src/org/jetbrains/kotlin/backend/common/lower/optimizations/LivenessAnalysis.kt @@ -0,0 +1,233 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.backend.common.lower.optimizations + +import org.jetbrains.kotlin.backend.common.copy +import org.jetbrains.kotlin.backend.common.forEachBit +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.declarations.IrDeclarationBase +import org.jetbrains.kotlin.ir.declarations.IrVariable +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.util.render +import org.jetbrains.kotlin.backend.common.ir.isUnconditional +import org.jetbrains.kotlin.ir.visitors.IrElementVisitor +import org.jetbrains.kotlin.ir.visitors.IrElementVisitorVoid +import org.jetbrains.kotlin.ir.visitors.acceptChildrenVoid +import java.util.* + +private fun BitSet.withBit(bit: Int) = if (get(bit)) this else copy().also { it.set(bit) } +private fun BitSet.withOutBit(bit: Int) = if (!get(bit)) this else copy().also { it.clear(bit) } + +object LivenessAnalysis { + fun run(body: IrBody, filter: (IrElement) -> Boolean) = + LivenessAnalysisVisitor(filter).run(body) + + private fun IrElement.getImmediateChildren(): List { + val result = mutableListOf() + acceptChildrenVoid(object : IrElementVisitorVoid { + override fun visitElement(element: IrElement) { + result.add(element) + // Do not recurse. + } + }) + return result + } + + /* + * The classic liveness analysis algorithm works on CFG and traverses it from bottom to top, + * this directly translates to the AST traversal from right to left. + * Each visitXXX takes live variables ~after~ the [element] and returns live variables ~before~ the [element]. + */ + private class LivenessAnalysisVisitor(val filter: (IrElement) -> Boolean) : IrElementVisitor { + private val variables = mutableListOf() + private val variableIds = mutableMapOf() + private val filteredElementEndsLV = mutableMapOf() + private val returnableBlockEndsLV = mutableMapOf() + private val loopEndsLV = mutableMapOf() + private val loopStartsLV = mutableMapOf() + private var catchesLV = BitSet() + + fun run(body: IrBody): Map> { + body.accept(this, BitSet() /* No variable is live at the end */) + return filteredElementEndsLV.mapValues { (_, liveVariables) -> + buildList { liveVariables.forEachBit { add(variables[it]) } } + } + } + + private fun getVariableId(variable: IrVariable) = variableIds.getOrPut(variable) { + variables.add(variable) + variables.lastIndex + } + + private inline fun saveAndCompute(element: T, liveVariables: BitSet, compute: () -> BitSet): BitSet { + if (filter(element)) + filteredElementEndsLV[element] = liveVariables.copy().also { it.or(catchesLV) } + return compute() + } + + override fun visitDeclaration(declaration: IrDeclarationBase, data: BitSet) = + error("Local declarations should've been popped out by this point") + + // Default: traverse the children in the reverse order, propagating live variables from right to left. + override fun visitElement(element: IrElement, data: BitSet) = saveAndCompute(element, data) { + val children = element.getImmediateChildren() + var liveVariables = data + for (i in children.size - 1 downTo 0) { + liveVariables = children[i].accept(this, liveVariables) + } + liveVariables + } + + override fun visitGetValue(expression: IrGetValue, data: BitSet) = saveAndCompute(expression, data) { + val variable = expression.symbol.owner as? IrVariable ?: return@saveAndCompute data + data.withBit(getVariableId(variable)) + } + + override fun visitVariable(declaration: IrVariable, data: BitSet) = saveAndCompute(declaration, data) { + val variableId = getVariableId(declaration) + var liveVariables = data.withOutBit(variableId) + liveVariables = declaration.initializer?.accept(this, liveVariables) ?: liveVariables + require(!liveVariables.get(variableId)) { "Use of uninitialized variable ${declaration.render()}" } + liveVariables + } + + override fun visitSetValue(expression: IrSetValue, data: BitSet) = saveAndCompute(expression, data) { + val variable = expression.symbol.owner as? IrVariable ?: error("Unexpected parameter rewrite: ${expression.render()}") + val liveVariables = data.withOutBit(getVariableId(variable)) + expression.value.accept(this, liveVariables) + } + + override fun visitReturn(expression: IrReturn, data: BitSet) = saveAndCompute(expression, data) { + val liveVariables = + (expression.returnTargetSymbol.owner as? IrReturnableBlock)?.let { + returnableBlockEndsLV[it] ?: error("Unknown return target ${expression.returnTargetSymbol.owner.render()}") + } ?: BitSet() // No variable is alive after returning from the function. + expression.value.accept(this, liveVariables) + } + + override fun visitContainerExpression(expression: IrContainerExpression, data: BitSet) = saveAndCompute(expression, data) { + (expression as? IrReturnableBlock)?.let { returnableBlock -> + returnableBlockEndsLV[returnableBlock] = data + } + visitElement(expression, data) + } + + /* + * (when) + * | + * +-> (cond1) + * | | + * | +--> (res1) --------+ + * | | + * +--> (cond2) | + * | | | + * | +--> (res2) ----+ + * | | | + * ................................... // more branches + * | | | + * v v v + * ( ....... next ....... ) + */ + override fun visitWhen(expression: IrWhen, data: BitSet) = saveAndCompute(expression, data) { + val isExhaustive = expression.branches.last().isUnconditional() + // An inexhaustive when clause can skip all the results and go to (next). + var liveVariables = if (isExhaustive) BitSet() else data + for (i in expression.branches.size - 1 downTo 0) { + val branch = expression.branches[i] + val conditionEndLV = liveVariables.copy() // (cond) was false. + conditionEndLV.or(branch.result.accept(this, data)) // (cond) was true. + liveVariables = branch.condition.accept(this, conditionEndLV) + } + liveVariables + } + + override fun visitThrow(expression: IrThrow, data: BitSet) = saveAndCompute(expression, data) { + expression.value.accept(this, catchesLV) // Assuming a throw might be caught by one of the nearest catch clauses. + } + + /* + * Here instead of precise but much more complicated solution, use simple approximation: + * assume an exception might be thrown anywhere from inside a try clause, and will be caught + * by either of the catch clauses. This implies that each alive variable at the start of any + * catch clauses must be alive anywhere inside the corresponding try clause (and at its start). + */ + override fun visitTry(aTry: IrTry, data: BitSet) = saveAndCompute(aTry, data) { + require(aTry.finallyExpression == null) { "All finally clauses should've been lowered" } + val currentCatchesLV = BitSet() // Live variables after try clause if there was an exception. + for (aCatch in aTry.catches) { + currentCatchesLV.or(aCatch.accept(this, data)) + } + val prevCatchesLV = catchesLV + catchesLV = currentCatchesLV + currentCatchesLV.or(aTry.tryResult.accept(this, data)) + catchesLV = prevCatchesLV + currentCatchesLV + } + + override fun visitBreak(jump: IrBreak, data: BitSet) = saveAndCompute(jump, data) { + loopEndsLV[jump.loop] ?: error("Break from an unknown loop ${jump.loop.render()}") + } + + override fun visitContinue(jump: IrContinue, data: BitSet) = saveAndCompute(jump, data) { + loopStartsLV[jump.loop] ?: error("Continue to an unknown loop ${jump.loop.render()}") + } + + /* + * +---- (body) <--+ + * | | | + * | b | | // continue goes to (cond) + * do-while loop: | r | | + * | e v | + * | a (cond) ---+ + * | k | + * | v + * +---> (next) + */ + override fun visitDoWhileLoop(loop: IrDoWhileLoop, data: BitSet) = saveAndCompute(loop, data) { + handleLoop(loop, data) + } + + /* + * For while loops we do the following transformation: + * while (condition) { body } -> if (condition) { do { body } while (condition) } + * + * (cond) ------+ + * | | + * v | + * +---- (body) <--+ | + * | | | | + * | b | | | // continue goes to (cond) + * while loop: | r | | | + * | e v | | + * | a (cond) ---+ | + * | k | | + * | v | + * +---> (next) <-----+ + */ + override fun visitWhileLoop(loop: IrWhileLoop, data: BitSet) = saveAndCompute(loop, data) { + val liveVariables = data.copy() // If no iteration has been executed. + liveVariables.or(handleLoop(loop, data)) // The actual loop body. + loop.condition.accept(this, liveVariables) + } + + private fun handleLoop(loop: IrLoop, data: BitSet): BitSet { + loopEndsLV[loop] = data + var bodyEndLV = loop.condition.accept(this, data) + val body = loop.body ?: return bodyEndLV + var bodyStartLV: BitSet + // In practice, only one or two iterations seem to be enough, but the classic algorithm + // loops until "saturation" (when nothing changes anymore). + do { + loopStartsLV[loop] = bodyEndLV + bodyStartLV = body.accept(this, bodyEndLV) + val nextBodyEndLV = loop.condition.accept(this, bodyStartLV) + val lvHaveChanged = nextBodyEndLV != bodyEndLV + bodyEndLV = nextBodyEndLV + } while (lvHaveChanged) + return bodyStartLV + } + } +} \ No newline at end of file