JVM optimize out temporary variables in bytecode

This commit is contained in:
Dmitry Petrov
2021-08-13 12:38:20 +03:00
committed by TeamCityServer
parent bddfd086f6
commit 041773fd25
22 changed files with 964 additions and 44 deletions
@@ -350,6 +350,116 @@ fun AbstractInsnNode?.insnText(insnList: InsnList): String {
}
}
fun MethodNode.dumpBody(): String {
val sw = StringWriter()
val pw = PrintWriter(sw)
fun LabelNode.labelRef() =
"L#${this@dumpBody.instructions.indexOf(this)}"
pw.println("${this.name} ${this.desc}")
for (tcb in this.tryCatchBlocks) {
pw.println(" TRYCATCHBLOCK start:${tcb.start.labelRef()} end:${tcb.end.labelRef()} handler:${tcb.handler.labelRef()}")
}
for ((i, insn) in this.instructions.toArray().withIndex()) {
when (insn.type) {
AbstractInsnNode.INSN ->
pw.println("$i\t${Printer.OPCODES[insn.opcode]}")
AbstractInsnNode.INT_INSN ->
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${(insn as IntInsnNode).operand}")
AbstractInsnNode.VAR_INSN ->
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${(insn as VarInsnNode).`var`}")
AbstractInsnNode.TYPE_INSN ->
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${(insn as TypeInsnNode).desc}")
AbstractInsnNode.FIELD_INSN -> {
val fieldInsn = insn as FieldInsnNode
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${fieldInsn.owner}#${fieldInsn.name} ${fieldInsn.desc}")
}
AbstractInsnNode.METHOD_INSN -> {
val methodInsn = insn as MethodInsnNode
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${methodInsn.owner}#${methodInsn.name} ${methodInsn.desc}")
}
AbstractInsnNode.INVOKE_DYNAMIC_INSN -> {
val indyInsn = insn as InvokeDynamicInsnNode
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${indyInsn.name} ${indyInsn.desc}")
val bsmTag = when (val tag = indyInsn.bsm.tag) {
Opcodes.H_GETFIELD -> "H_GETFIELD"
Opcodes.H_GETSTATIC -> "H_GETSTATIC"
Opcodes.H_PUTFIELD -> "H_PUTFIELD"
Opcodes.H_PUTSTATIC -> "H_PUTSTATIC"
Opcodes.H_INVOKEVIRTUAL -> "H_INVOKEVIRTUAL"
Opcodes.H_INVOKESTATIC -> "H_INVOKESTATIC"
Opcodes.H_INVOKESPECIAL -> "H_INVOKESPECIAL"
Opcodes.H_NEWINVOKESPECIAL -> "H_NEWINVOKESPECIAL"
Opcodes.H_INVOKEINTERFACE -> "H_INVOKEINTERFACE"
else -> "<$tag>"
}
pw.println("\t$bsmTag ${indyInsn.bsm.owner}#${indyInsn.bsm.name} ${indyInsn.bsm.desc} [")
for (bsmArg in indyInsn.bsmArgs) {
pw.println("\t$bsmArg")
}
pw.println("\t]")
}
AbstractInsnNode.JUMP_INSN ->
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${(insn as JumpInsnNode).label.labelRef()}")
AbstractInsnNode.LABEL ->
pw.println("$i\tL#$i")
AbstractInsnNode.LDC_INSN ->
pw.println("$i\tLDC ${(insn as LdcInsnNode).cst}")
AbstractInsnNode.IINC_INSN -> {
val iincInsn = insn as IincInsnNode
pw.println("$i\tIINC ${iincInsn.`var`} incr:${iincInsn.incr}")
}
AbstractInsnNode.TABLESWITCH_INSN -> {
val switchInsn = insn as TableSwitchInsnNode
pw.println("$i\tTABLESWITCH min:${switchInsn.min} max:${switchInsn.max}{")
for (k in switchInsn.labels.indices) {
pw.println("\t${k + switchInsn.min}: ${switchInsn.labels[k].labelRef()}")
}
pw.println("\tdefault: ${switchInsn.dflt.labelRef()}")
pw.println("\t}")
}
AbstractInsnNode.LOOKUPSWITCH_INSN -> {
val switchInsn = insn as LookupSwitchInsnNode
pw.println("$i\tLOOKUPSWITCH {")
for (k in switchInsn.labels.indices) {
val key = switchInsn.keys[k]
val label = switchInsn.labels[k]
pw.println("\t$key: ${label.labelRef()}")
}
pw.println("\t}")
}
AbstractInsnNode.MULTIANEWARRAY_INSN -> {
val manInsn = insn as MultiANewArrayInsnNode
pw.println("$i\t${Printer.OPCODES[insn.opcode]} ${manInsn.desc} dims:${manInsn.dims} ")
}
AbstractInsnNode.FRAME -> {
// TODO dump frame if needed
pw.println("$i\tFRAME {...}")
}
AbstractInsnNode.LINE -> {
val lineInsn = insn as LineNumberNode
pw.println("$i\tLINENUMBER ${lineInsn.line} ${lineInsn.start.labelRef()}")
}
else -> {
pw.println("$i\t??? $insn")
}
}
}
for (lv in this.localVariables) {
pw.println(" LOCALVARIABLE ${lv.index} ${lv.name} ${lv.desc} ${lv.start.labelRef()} ${lv.end.labelRef()}")
if (lv.signature != null) {
pw.println(" // signature: ${lv.signature}")
}
}
pw.flush()
return sw.toString()
}
internal val AbstractInsnNode?.insnOpcodeText: String
get() = when (this) {
null -> "null"
@@ -23,6 +23,7 @@ import org.jetbrains.kotlin.codegen.optimization.boxing.RedundantBoxingMethodTra
import org.jetbrains.kotlin.codegen.optimization.boxing.StackPeepholeOptimizationsTransformer
import org.jetbrains.kotlin.codegen.optimization.common.prepareForEmitting
import org.jetbrains.kotlin.codegen.optimization.nullCheck.RedundantNullCheckMethodTransformer
import org.jetbrains.kotlin.codegen.optimization.temporaryVals.CodeCompactionTransformer
import org.jetbrains.kotlin.codegen.optimization.transformer.CompositeMethodTransformer
import org.jetbrains.kotlin.codegen.state.GenerationState
import org.jetbrains.org.objectweb.asm.MethodVisitor
@@ -47,6 +48,7 @@ class OptimizationMethodVisitor(
)
val optimizationTransformer = CompositeMethodTransformer(
CodeCompactionTransformer(),
CapturedVarsOptimizationMethodTransformer(),
RedundantNullCheckMethodTransformer(generationState),
RedundantCheckCastEliminationMethodTransformer(),
@@ -28,7 +28,7 @@ import org.jetbrains.org.objectweb.asm.tree.TypeInsnNode
class RedundantCheckCastEliminationMethodTransformer : MethodTransformer() {
override fun transform(internalClassName: String, methodNode: MethodNode) {
val insns = methodNode.instructions.toArray()
if (!insns.any { it.opcode == Opcodes.CHECKCAST}) return
if (!insns.any { it.opcode == Opcodes.CHECKCAST }) return
if (insns.any { ReifiedTypeInliner.isOperationReifiedMarker(it) }) return
val redundantCheckCasts = ArrayList<TypeInsnNode>()
@@ -118,9 +118,9 @@ open class FastMethodAnalyzer<V : Value>(
}
} catch (e: AnalyzerException) {
throw AnalyzerException(e.node, "Error at instruction #$insn ${insnNode.insnText}: ${e.message}", e)
throw AnalyzerException(e.node, "Error at instruction #$insn ${insnNode.insnText(method.instructions)}: ${e.message}", e)
} catch (e: Exception) {
throw AnalyzerException(insnNode, "Error at instruction #$insn ${insnNode.insnText}: ${e.message}", e)
throw AnalyzerException(insnNode, "Error at instruction #$insn ${insnNode.insnText(method.instructions)}: ${e.message}", e)
}
}
@@ -0,0 +1,409 @@
/*
* Copyright 2010-2021 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.codegen.optimization.temporaryVals
import org.jetbrains.kotlin.codegen.optimization.DeadCodeEliminationMethodTransformer
import org.jetbrains.kotlin.codegen.optimization.common.InsnSequence
import org.jetbrains.kotlin.codegen.optimization.common.removeUnusedLocalVariables
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer
import org.jetbrains.kotlin.utils.SmartList
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.*
import kotlin.math.max
class CodeCompactionTransformer : MethodTransformer() {
private val temporaryValsAnalyzer = TemporaryValsAnalyzer()
private val deadCodeElimination = DeadCodeEliminationMethodTransformer()
override fun transform(internalClassName: String, methodNode: MethodNode) {
simplifyControlFlow(methodNode)
val cfg = ControlFlowGraph(methodNode)
processLabels(cfg)
simplifyKnownSafeCallPatterns(cfg)
val temporaryVals = temporaryValsAnalyzer.analyze(internalClassName, methodNode)
if (temporaryVals.isNotEmpty()) {
optimizeTemporaryVals(cfg, temporaryVals)
}
methodNode.stripTemporaryValInitMarkers()
methodNode.removeUnusedLocalVariables()
}
private class ControlFlowGraph(val methodNode: MethodNode) {
private val nonTrivialPredecessors = HashMap<LabelNode, MutableList<AbstractInsnNode>>()
fun reset() {
nonTrivialPredecessors.clear()
}
fun addNonTrivialPredecessor(label: LabelNode, pred: AbstractInsnNode) {
nonTrivialPredecessors.getOrPut(label) { SmartList() }.add(pred)
}
fun hasNonTrivialPredecessors(label: LabelNode) =
nonTrivialPredecessors.containsKey(label)
fun hasSinglePredecessor(label: LabelNode, expectedPredecessor: AbstractInsnNode): Boolean {
var trivialPredecessor = label.previous
if (trivialPredecessor.opcode == Opcodes.GOTO ||
trivialPredecessor.opcode in Opcodes.IRETURN..Opcodes.RETURN ||
trivialPredecessor.opcode == Opcodes.ATHROW
) {
// Previous instruction is not a predecessor in CFG
trivialPredecessor = null
} else {
// Check trivial predecessor
if (trivialPredecessor != expectedPredecessor) return false
}
val nonTrivialPredecessors = nonTrivialPredecessors[label]
?: return trivialPredecessor != null
return when {
nonTrivialPredecessors.size > 1 ->
false
nonTrivialPredecessors.size == 0 ->
trivialPredecessor == expectedPredecessor
else ->
trivialPredecessor == null && nonTrivialPredecessors[0] == expectedPredecessor
}
}
}
private fun processLabels(cfg: ControlFlowGraph) {
cfg.reset()
val methodNode = cfg.methodNode
val insnList = methodNode.instructions
val usedLabels = HashSet<LabelNode>()
val first = insnList.first
if (first is LabelNode) {
usedLabels.add(first)
}
val last = insnList.last
if (last is LabelNode) {
usedLabels.add(last)
}
fun addCfgEdgeToLabel(from: AbstractInsnNode, label: LabelNode) {
usedLabels.add(label)
cfg.addNonTrivialPredecessor(label, from)
}
fun addCfgEdgesToLabels(from: AbstractInsnNode, labels: Collection<LabelNode>) {
usedLabels.addAll(labels)
for (label in labels) {
cfg.addNonTrivialPredecessor(label, from)
}
}
for (insn in insnList) {
when (insn.type) {
AbstractInsnNode.LINE -> {
usedLabels.add((insn as LineNumberNode).start)
}
AbstractInsnNode.JUMP_INSN -> {
addCfgEdgeToLabel(insn, (insn as JumpInsnNode).label)
}
AbstractInsnNode.LOOKUPSWITCH_INSN -> {
val switchInsn = insn as LookupSwitchInsnNode
addCfgEdgeToLabel(insn, switchInsn.dflt)
addCfgEdgesToLabels(insn, switchInsn.labels)
}
AbstractInsnNode.TABLESWITCH_INSN -> {
val switchInsn = insn as TableSwitchInsnNode
addCfgEdgeToLabel(insn, switchInsn.dflt)
addCfgEdgesToLabels(insn, switchInsn.labels)
}
}
}
for (lv in methodNode.localVariables) {
usedLabels.add(lv.start)
usedLabels.add(lv.end)
}
for (tcb in methodNode.tryCatchBlocks) {
usedLabels.add(tcb.start)
usedLabels.add(tcb.end)
addCfgEdgeToLabel(tcb.start, tcb.handler)
}
var insn = insnList.first
while (insn != null) {
insn = if (insn is LabelNode && insn !in usedLabels) {
val next = insn.next
insnList.remove(insn)
next
} else {
insn.next
}
}
}
private fun optimizeTemporaryVals(cfg: ControlFlowGraph, temporaryVals: List<TemporaryVal>) {
val insnList = cfg.methodNode.instructions
for (tmp in temporaryVals) {
if (tmp.loadInsns.size == 1) {
val storeInsn = tmp.storeInsn
val loadInsn = tmp.loadInsns[0]
if (storeInsn.next == loadInsn || InsnSequence(storeInsn.next, loadInsn).none { it.isIntervening(cfg) }) {
// If there are no intervening instructions between store and load,
// drop both store and load, just keep intermediate value on stack.
// This approximately corresponds to some receiver stored in a temporary variable and immediately loaded
// (e.g., in safe call)
insnList.remove(storeInsn)
insnList.remove(loadInsn)
continue
}
if (storeInsn.matchOpcodes(Opcodes.ASTORE, Opcodes.ALOAD, Opcodes.ALOAD)) {
val aLoad1 = storeInsn.next as VarInsnNode
val aLoad2 = aLoad1.next as VarInsnNode
if (aLoad2 == loadInsn) {
// Replace instruction sequence:
// ASTORE tmp
// ALOAD x
// ALOAD tmp
// with:
// ALOAD x
// SWAP
// This approximately corresponds to some extension receiver stored in a temporary variable and immediately loaded
// (e.g., in safe call).
insnList.remove(storeInsn)
insnList.remove(loadInsn)
insnList.insert(aLoad1, InsnNode(Opcodes.SWAP))
continue
}
}
if (storeInsn.matchOpcodes(Opcodes.ASTORE, Opcodes.GETSTATIC, Opcodes.ALOAD)) {
val getStaticInsn = storeInsn.next as FieldInsnNode
val aLoad2 = getStaticInsn.next as VarInsnNode
if (aLoad2 == loadInsn && Type.getType(getStaticInsn.desc).size == 1) {
// Replace instruction sequence:
// ASTORE tmp
// GETSTATIC ... // size 1
// ALOAD tmp
// with:
// GETSTATIC ... // size 1
// SWAP
// This approximately corresponds to some extension receiver stored in a temporary variable and immediately loaded
// (e.g., in safe call).
insnList.remove(storeInsn)
insnList.remove(loadInsn)
insnList.insert(getStaticInsn, InsnNode(Opcodes.SWAP))
continue
}
}
}
}
}
private fun simplifyControlFlow(methodNode: MethodNode) {
val insnList = methodNode.instructions
var needsDCE = false
for (insn in insnList.toArray()) {
if (insn.opcode == Opcodes.NOP) {
// Remove NOPs not preceded by LABEL or LINENUMBER instructions.
val prev = insn.previous ?: continue
if (prev.type == AbstractInsnNode.LABEL || prev.type == AbstractInsnNode.LINE) continue
insnList.remove(insn)
continue
}
if (insn.opcode == Opcodes.GOTO) {
// If we have a GOTO instruction that leads to another GOTO, replace corresponding label.
val jumpInsn = insn as JumpInsnNode
val newLabel = jumpInsn.getFinalLabel()
if (newLabel != jumpInsn.label) {
needsDCE = true
}
jumpInsn.label = newLabel
}
}
if (needsDCE) {
deadCodeElimination.transform("<fake>", methodNode)
}
}
@Suppress("DuplicatedCode")
private fun simplifyKnownSafeCallPatterns(cfg: ControlFlowGraph) {
val insnList = cfg.methodNode.instructions
var maxStackIncrement = 0
for (insn in insnList.toArray()) {
if (insn.matchOpcodes(Opcodes.ALOAD, Opcodes.IFNONNULL)) {
// This looks like a start of some safe call:
// In a safe call, we introduce a temporary variable for a safe receiver, which is usually loaded twice:
// one time for a null check (1) and another time for an actual call (2):
// { ... evaluate receiver ... }
// ASTORE v
// ALOAD v
// IFNONNULL L
// { ... if null ... }
// L:
// [ possible first receiver - ALOAD or GETSTATIC ]
// ALOAD v
// { ... call ... }
// Try to remove a load instruction for (2), so that the safe call would look like:
// { ... evaluate receiver ... }
// ASTORE v
// ALOAD v
// DUP
// IFNONNULL L
// POP
// { ... if null ... }
// L:
// [ possible dispatch receiver - ALOAD or GETSTATIC ]
// [ SWAP if there was a dispatch receiver ]
// { ... call ... }
val aLoad1 = insn as VarInsnNode
val ifNonNull = insn.next as JumpInsnNode
val label1 = ifNonNull.label
if (!cfg.hasSinglePredecessor(ifNonNull.label, ifNonNull)) continue
val label1Next = label1.next ?: continue
if (label1Next.opcode == Opcodes.ALOAD) {
val aLoad2 = label1Next as VarInsnNode
if (aLoad2.`var` == aLoad1.`var`) {
// Rewrite:
// ALOAD v
// IFNONNULL L
// ...
// L:
// ALOAD v
// to:
// ALOAD v
// DUP
// IFNONNULL L
// POP
// ...
// L:
// ...
insnList.insertBefore(ifNonNull, InsnNode(Opcodes.DUP))
insnList.insert(ifNonNull, InsnNode(Opcodes.POP))
insnList.remove(aLoad2)
maxStackIncrement = max(maxStackIncrement, 1)
continue
} else {
val aLoad2Next = aLoad2.next
if (aLoad2Next.opcode == Opcodes.ALOAD) {
val aLoad3 = aLoad2Next as VarInsnNode
if (aLoad3.`var` == aLoad1.`var`) {
// Rewrite:
// ALOAD v
// IFNONNULL L
// ...
// L:
// ALOAD x
// ALOAD v
// to:
// ALOAD v
// DUP
// IFNONNULL L
// POP
// L:
// ALOAD x
// SWAP
insnList.insertBefore(ifNonNull, InsnNode(Opcodes.DUP))
insnList.insert(ifNonNull, InsnNode(Opcodes.POP))
insnList.insert(aLoad2, InsnNode(Opcodes.SWAP))
insnList.remove(aLoad3)
maxStackIncrement = max(maxStackIncrement, 1)
continue
}
}
}
} else if (label1Next.matchOpcodes(Opcodes.GETSTATIC, Opcodes.ALOAD)) {
val getStaticInsn = label1Next as FieldInsnNode
val aLoad3 = getStaticInsn.next as VarInsnNode
if (Type.getType(getStaticInsn.desc).size == 1 && aLoad3.`var` == aLoad1.`var`) {
// Rewrite:
// ALOAD v
// IFNONNULL L
// ...
// L:
// GETSTATIC ... // size 1
// ALOAD v
// to:
// ALOAD v
// DUP
// IFNONNULL L
// POP
// L:
// GETSTATIC ... // size 1
// SWAP
insnList.insertBefore(ifNonNull, InsnNode(Opcodes.DUP))
insnList.insert(ifNonNull, InsnNode(Opcodes.POP))
insnList.insert(getStaticInsn, InsnNode(Opcodes.SWAP))
insnList.remove(aLoad3)
maxStackIncrement = max(maxStackIncrement, 1)
continue
}
}
}
}
cfg.methodNode.maxStack += maxStackIncrement
}
private fun JumpInsnNode.getFinalLabel(): LabelNode {
var label = this.label
var insn = label.next
while (true) {
when {
insn.type == AbstractInsnNode.LABEL || insn.type == AbstractInsnNode.LINE -> {
insn = insn.next ?: break
}
insn.opcode == Opcodes.GOTO -> {
val newLabel = (insn as JumpInsnNode).label
if (newLabel == label) return newLabel
label = newLabel
insn = label.next ?: break
}
insn.opcode == Opcodes.NOP -> {
insn = insn.next ?: break
}
else -> break
}
}
return label
}
private fun AbstractInsnNode.matchOpcodes(vararg opcodes: Int): Boolean {
var insn = this
for (i in opcodes.indices) {
if (insn.opcode != opcodes[i]) return false
insn = insn.next ?: return false
}
return true
}
private fun AbstractInsnNode.isIntervening(context: ControlFlowGraph): Boolean =
when (this.type) {
AbstractInsnNode.LINE, AbstractInsnNode.FRAME ->
false
AbstractInsnNode.LABEL ->
context.hasNonTrivialPredecessors(this as LabelNode)
AbstractInsnNode.INSN ->
this.opcode != Opcodes.NOP
else ->
true
}
}
@@ -0,0 +1,184 @@
/*
* Copyright 2010-2021 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.codegen.optimization.temporaryVals
import org.jetbrains.kotlin.codegen.inline.INLINE_MARKER_CLASS_NAME
import org.jetbrains.kotlin.codegen.optimization.common.FastMethodAnalyzer
import org.jetbrains.kotlin.codegen.optimization.common.isLoadOperation
import org.jetbrains.kotlin.codegen.optimization.common.isStoreOperation
import org.jetbrains.kotlin.codegen.optimization.fixStack.BasicTypeInterpreter
import org.jetbrains.kotlin.codegen.optimization.removeNodeGetNext
import org.jetbrains.kotlin.utils.SmartSet
import org.jetbrains.org.objectweb.asm.Handle
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.commons.InstructionAdapter
import org.jetbrains.org.objectweb.asm.tree.AbstractInsnNode
import org.jetbrains.org.objectweb.asm.tree.MethodInsnNode
import org.jetbrains.org.objectweb.asm.tree.MethodNode
import org.jetbrains.org.objectweb.asm.tree.VarInsnNode
import org.jetbrains.org.objectweb.asm.tree.analysis.Value
private const val TEMPORARY_VAL_INIT_MARKER = "<TEMPORARY-VAL-INIT>"
fun InstructionAdapter.addTemporaryValInitMarker() {
invokestatic(INLINE_MARKER_CLASS_NAME, TEMPORARY_VAL_INIT_MARKER, "()V", false)
}
fun AbstractInsnNode.isTemporaryValInitMarker(): Boolean {
if (opcode != Opcodes.INVOKESTATIC) return false
val methodInsn = this as MethodInsnNode
return methodInsn.owner == INLINE_MARKER_CLASS_NAME && methodInsn.name == TEMPORARY_VAL_INIT_MARKER
}
fun MethodNode.stripTemporaryValInitMarkers() {
var insn = this.instructions.first
while (insn != null) {
insn = if (insn.isTemporaryValInitMarker()) {
this.instructions.removeNodeGetNext(insn)
} else {
insn.next
}
}
}
// A temporary val is a local variables that is:
// - initialized once (with some xSTORE instruction)
// - is not written by any other instruction (xSTORE or IINC)
// - not "observed" by any LVT entry
class TemporaryVal(
val index: Int,
val storeInsn: VarInsnNode,
val loadInsns: List<VarInsnNode>
)
class TemporaryValsAnalyzer {
fun analyze(internalClassName: String, methodNode: MethodNode): List<TemporaryVal> {
val temporaryValStores = methodNode.instructions.filter { it.isStoreOperation() && it.previous.isTemporaryValInitMarker() }
if (temporaryValStores.isEmpty()) return emptyList()
val storeInsnToStoreData = temporaryValStores.associateWith { StoreData(it) }
FastMethodAnalyzer(internalClassName, methodNode, StoreTrackingInterpreter(storeInsnToStoreData)).analyze()
return storeInsnToStoreData.values
.filterNot { it.isDirty }
.map {
val storeInsn = it.storeInsn as VarInsnNode
val loadInsns = it.loads.map { load -> load as VarInsnNode }
TemporaryVal(storeInsn.`var`, storeInsn, loadInsns)
}
.sortedBy { methodNode.instructions.indexOf(it.storeInsn) }
}
private class StoreData(val storeInsn: AbstractInsnNode) {
var isDirty = false
val storeOpcode = storeInsn.opcode
val value =
StoredValue.Store(
this,
when (storeOpcode) {
Opcodes.LSTORE, Opcodes.DSTORE -> 2
else -> 1
}
)
val loads = LinkedHashSet<AbstractInsnNode>()
}
private sealed class StoredValue(private val _size: Int) : Value {
override fun getSize(): Int = _size
object Small : StoredValue(1)
object Big : StoredValue(2)
class Store(val temporaryVal: StoreData, size: Int) : StoredValue(size) {
override fun equals(other: Any?): Boolean =
other is Store && other.temporaryVal === temporaryVal
override fun hashCode(): Int =
temporaryVal.hashCode()
}
class DirtyStore(val temporaryVals: Collection<StoreData>, size: Int) : StoredValue(size) {
override fun equals(other: Any?): Boolean =
other is DirtyStore && other.temporaryVals == temporaryVals
override fun hashCode(): Int =
temporaryVals.hashCode()
}
}
private class StoreTrackingInterpreter(
private val storeInsnToStoreData: Map<AbstractInsnNode, StoreData>
) : BasicTypeInterpreter<StoredValue>() {
override fun uninitializedValue(): StoredValue = StoredValue.Small
override fun booleanValue(): StoredValue = StoredValue.Small
override fun charValue(): StoredValue = StoredValue.Small
override fun byteValue(): StoredValue = StoredValue.Small
override fun shortValue(): StoredValue = StoredValue.Small
override fun intValue(): StoredValue = StoredValue.Small
override fun longValue(): StoredValue = StoredValue.Big
override fun floatValue(): StoredValue = StoredValue.Small
override fun doubleValue(): StoredValue = StoredValue.Big
override fun nullValue(): StoredValue = StoredValue.Small
override fun objectValue(type: Type): StoredValue = StoredValue.Small
override fun arrayValue(type: Type): StoredValue = StoredValue.Small
override fun methodValue(type: Type): StoredValue = StoredValue.Small
override fun handleValue(handle: Handle): StoredValue = StoredValue.Small
override fun typeConstValue(typeConst: Type): StoredValue = StoredValue.Small
override fun aaLoadValue(arrayValue: StoredValue): StoredValue = StoredValue.Small
override fun copyOperation(insn: AbstractInsnNode, value: StoredValue): StoredValue {
if (insn.isStoreOperation()) {
val temporaryValData = storeInsnToStoreData[insn]
if (temporaryValData != null) {
return temporaryValData.value
}
} else if (insn.isLoadOperation()) {
if (value is StoredValue.DirtyStore) {
// If we load a dirty value, invalidate all related temporary vals.
for (temporaryValData in value.temporaryVals) {
temporaryValData.isDirty = true
}
} else if (value is StoredValue.Store) {
// Keep track of a load instruction
value.temporaryVal.loads.add(insn)
}
}
return value
}
override fun merge(a: StoredValue, b: StoredValue): StoredValue {
return when {
a === b ->
a
a is StoredValue.Store || a is StoredValue.DirtyStore || b is StoredValue.Store || b is StoredValue.DirtyStore -> {
// 'StoreValue.Store' are unique for each 'StoreData', so if we are here, we are going to merge value stored
// by a xSTORE instruction with some other value (maybe produced by another xSTORE instruction).
// Loading such value invalidates all related temporary vals.
val dirtySet = SmartSet.create(a.temporaryVals())
dirtySet.addAll(b.temporaryVals())
StoredValue.DirtyStore(dirtySet, a.size)
}
else ->
StoredValue.Small
}
}
private fun StoredValue.temporaryVals(): Collection<StoreData> =
when (this) {
is StoredValue.Store -> setOf(this.temporaryVal)
is StoredValue.DirtyStore -> this.temporaryVals
else -> emptySet()
}
}
}
@@ -725,9 +725,21 @@ public class FirBytecodeTextTestGenerated extends AbstractFirBytecodeTextTest {
}
@Test
@TestMetadata("safeCallToPrimitiveEquality.kt")
public void testSafeCallToPrimitiveEquality() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality.kt");
@TestMetadata("safeCallToPrimitiveEquality1.kt")
public void testSafeCallToPrimitiveEquality1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality1.kt");
}
@Test
@TestMetadata("safeCallToPrimitiveEquality2.kt")
public void testSafeCallToPrimitiveEquality2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality2.kt");
}
@Test
@TestMetadata("safeCallToPrimitiveEquality3.kt")
public void testSafeCallToPrimitiveEquality3() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality3.kt");
}
@Test
@@ -3231,6 +3243,40 @@ public class FirBytecodeTextTestGenerated extends AbstractFirBytecodeTextTest {
}
}
@Nested
@TestMetadata("compiler/testData/codegen/bytecodeText/ifNullChain")
@TestDataPath("$PROJECT_ROOT")
public class IfNullChain {
@Test
public void testAllFilesPresentInIfNullChain() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/bytecodeText/ifNullChain"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true);
}
@Test
@TestMetadata("safeCallChain1.kt")
public void testSafeCallChain1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChain1.kt");
}
@Test
@TestMetadata("safeCallChain2.kt")
public void testSafeCallChain2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChain2.kt");
}
@Test
@TestMetadata("safeCallChainMemberExt1.kt")
public void testSafeCallChainMemberExt1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChainMemberExt1.kt");
}
@Test
@TestMetadata("safeCallChainMemberExt2.kt")
public void testSafeCallChainMemberExt2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChainMemberExt2.kt");
}
}
@Nested
@TestMetadata("compiler/testData/codegen/bytecodeText/inline")
@TestDataPath("$PROJECT_ROOT")
@@ -28,6 +28,7 @@ import org.jetbrains.kotlin.codegen.inline.ReifiedTypeInliner.Companion.putNeedC
import org.jetbrains.kotlin.codegen.inline.ReifiedTypeInliner.OperationKind.AS
import org.jetbrains.kotlin.codegen.inline.ReifiedTypeInliner.OperationKind.SAFE_AS
import org.jetbrains.kotlin.codegen.intrinsics.TypeIntrinsics
import org.jetbrains.kotlin.codegen.optimization.temporaryVals.addTemporaryValInitMarker
import org.jetbrains.kotlin.codegen.pseudoInsns.fakeAlwaysFalseIfeq
import org.jetbrains.kotlin.codegen.pseudoInsns.fixStackAndJump
import org.jetbrains.kotlin.codegen.signature.BothSignatureWriter
@@ -629,6 +630,9 @@ class ExpressionCodegen(
initializer.markLineNumber(startOffset = true)
value.materializeAt(varType, declaration.type)
declaration.markLineNumber(startOffset = true)
if (declaration.isTemporaryVal()) {
mv.addTemporaryValInitMarker()
}
mv.store(index, varType)
} else if (declaration.isVisibleInLVT) {
pushDefaultValueOnStack(varType, mv)
@@ -646,6 +650,11 @@ class ExpressionCodegen(
return MaterialValue(this, type, expression.symbol.owner.realType)
}
private fun IrValueDeclaration.isTemporaryVal() =
this is IrVariable &&
!this.isVar &&
this.origin == IrDeclarationOrigin.IR_TEMPORARY_VARIABLE
internal fun genOrGetLocal(expression: IrExpression, type: Type, parameterType: IrType, data: BlockInfo): StackValue =
if (expression is IrGetValue)
StackValue.local(
@@ -766,7 +775,7 @@ class ExpressionCodegen(
val index = frameMap.getIndex(irSymbol)
if (index >= 0)
return index
throw AssertionError("Non-mapped local declaration: $irSymbol\n in ${irFunction.dump()}")
throw AssertionError("Non-mapped local declaration: ${irSymbol.owner.dump()}\n in ${irFunction.dump()}")
}
private fun handlePlusMinus(expression: IrSetValue, value: IrExpression?, isMinus: Boolean): Boolean {
@@ -57,9 +57,8 @@ class IrCallImpl(
valueArgumentsCount: Int = symbol.descriptor.valueParameters.size,
origin: IrStatementOrigin? = null,
superQualifierSymbol: IrClassSymbol? = null,
) = IrCallImpl(
startOffset, endOffset, type, symbol, typeArgumentsCount, valueArgumentsCount, origin, superQualifierSymbol
)
) =
IrCallImpl(startOffset, endOffset, type, symbol, typeArgumentsCount, valueArgumentsCount, origin, superQualifierSymbol)
fun fromSymbolOwner(
startOffset: Int,
@@ -70,8 +69,24 @@ class IrCallImpl(
valueArgumentsCount: Int = symbol.owner.valueParameters.size,
origin: IrStatementOrigin? = null,
superQualifierSymbol: IrClassSymbol? = null,
) = IrCallImpl(
startOffset, endOffset, type, symbol, typeArgumentsCount, valueArgumentsCount, origin, superQualifierSymbol
)
) =
IrCallImpl(startOffset, endOffset, type, symbol, typeArgumentsCount, valueArgumentsCount, origin, superQualifierSymbol)
fun fromSymbolOwner(
startOffset: Int,
endOffset: Int,
symbol: IrSimpleFunctionSymbol
) =
IrCallImpl(
startOffset,
endOffset,
symbol.owner.returnType,
symbol,
typeArgumentsCount = symbol.owner.typeParameters.size,
valueArgumentsCount = symbol.owner.valueParameters.size,
origin = null,
superQualifierSymbol = null
)
}
}
@@ -1,12 +1,3 @@
// FILE: J.java
import org.jetbrains.annotations.NotNull;
public interface J {
@NotNull
public Integer foo();
}
// FILE: test.kt
fun Long.id() = this
fun Short.id() = this
@@ -15,29 +6,20 @@ fun String.drop2() = if (length >= 2) subSequence(2, length) else null
fun doSimple1(s: String?) = s?.length == 3
fun doJava1(s: String?, j: J) = s?.length == j.foo()
fun doLongReceiver1(x: Long?) = x?.id() == 3L
fun doShortReceiver1(x: Short?, y: Short) = x?.id() == y
fun doChain1(s: String?) = s?.drop2()?.length == 1
fun doIf1(s: String?) =
if (s?.length == 1) "A" else "B"
fun doSimple2(s: String?) = 3 == s?.length
fun doJava2(s: String?, j: J) = j.foo() == s?.length
fun doLongReceiver2(x: Long?) = 3L == x?.id()
fun doShortReceiver2(x: Short?, y: Short) = y == x?.id()
fun doChain2(s: String?) = 1 == s?.drop2()?.length
fun doIf2(s: String?) =
if (1 == s?.length) "A" else "B"
// `doJava1`/`doJava2` box `s?.length` instead of unboxing `j.foo()`:
// 2 valueOf
// 0 valueOf
@@ -0,0 +1,7 @@
fun String.drop2() = if (length >= 2) subSequence(2, length) else null
fun doChain1(s: String?) = s?.drop2()?.length == 1
fun doChain2(s: String?) = 1 == s?.drop2()?.length
// 0 valueOf
@@ -0,0 +1,16 @@
// FILE: J.java
import org.jetbrains.annotations.NotNull;
public interface J {
@NotNull
public Integer foo();
}
// FILE: safeCallToPrimitiveEquality3.kt
fun doJava1(s: String?, j: J) = s?.length == j.foo()
fun doJava2(s: String?, j: J) = j.foo() == s?.length
// `doJava1`/`doJava2` box `s?.length` instead of unboxing `j.foo()`:
// 2 valueOf
@@ -13,5 +13,5 @@ fun test(x: A?) {
// 0 ACONST_NULL
// JVM_IR_TEMPLATES
// 1 POP
// 2 POP
// 0 ACONST_NULL
@@ -24,4 +24,4 @@ fun test(ss: List<String?>) {
// 2 POP
// JVM_IR_TEMPLATES
// 1 POP
// 2 POP
@@ -14,12 +14,12 @@ fun main(args: Array<String>) {
@BuilderInference
suspend fun SequenceScope<Int>.awaitSeq(): Int = 42
// 1 LINENUMBER 9 L18
// JVM_IR_TEMPLATES
// 1 LINENUMBER 8 L13
// 1 LOCALVARIABLE a I L[0-9]+ L4
// JVM_TEMPLATES
// 1 LINENUMBER 9 L18
// 1 LOCALVARIABLE a I L[0-9]+ L18
// IGNORE_BACKEND_FIR: JVM_IR
@@ -12,6 +12,6 @@ fun f() {
// 1 LOCALVARIABLE c C L3 L\d+ 0
// JVM_IR_TEMPLATES
// 1 ISTORE 2\s+L4
// 1 ISTORE 2\s+L3
// 1 ILOAD 2\s+INVOKEVIRTUAL java/io/PrintStream.print \(C\)V
// 1 LOCALVARIABLE c C L4 L\d+ 2
// 1 LOCALVARIABLE c C L3 L\d+ 2
@@ -0,0 +1,8 @@
class A(val b: B)
class B(val c: C)
class C(val s: String)
fun test(an: A?) = an?.b?.c?.s
// JVM_IR_TEMPLATES
// 0 ASTORE
@@ -0,0 +1,8 @@
class A(val bn: B?)
class B(val cn: C?)
class C(val s: String)
fun test(an: A?) = an?.bn?.cn?.s
// JVM_IR_TEMPLATES
// 0 ASTORE
@@ -0,0 +1,14 @@
class A
class B
class C
object Host {
val A.b: B get() = B()
val B.c: C get() = C()
val C.s: String get() = "s"
fun test(an: A?) = an?.b?.c?.s
}
// JVM_IR_TEMPLATES
// 0 ASTORE
@@ -0,0 +1,18 @@
import Host.b
import Host.c
import Host.s
class A
class B
class C
object Host {
val A.b: B get() = B()
val B.c: C get() = C()
val C.s: String get() = "s"
}
fun test(an: A?) = an?.b?.c?.s
// JVM_IR_TEMPLATES
// 0 ASTORE
@@ -713,9 +713,21 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
}
@Test
@TestMetadata("safeCallToPrimitiveEquality.kt")
public void testSafeCallToPrimitiveEquality() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality.kt");
@TestMetadata("safeCallToPrimitiveEquality1.kt")
public void testSafeCallToPrimitiveEquality1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality1.kt");
}
@Test
@TestMetadata("safeCallToPrimitiveEquality2.kt")
public void testSafeCallToPrimitiveEquality2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality2.kt");
}
@Test
@TestMetadata("safeCallToPrimitiveEquality3.kt")
public void testSafeCallToPrimitiveEquality3() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality3.kt");
}
@Test
@@ -3087,6 +3099,40 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
}
}
@Nested
@TestMetadata("compiler/testData/codegen/bytecodeText/ifNullChain")
@TestDataPath("$PROJECT_ROOT")
public class IfNullChain {
@Test
public void testAllFilesPresentInIfNullChain() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/bytecodeText/ifNullChain"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM, true);
}
@Test
@TestMetadata("safeCallChain1.kt")
public void testSafeCallChain1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChain1.kt");
}
@Test
@TestMetadata("safeCallChain2.kt")
public void testSafeCallChain2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChain2.kt");
}
@Test
@TestMetadata("safeCallChainMemberExt1.kt")
public void testSafeCallChainMemberExt1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChainMemberExt1.kt");
}
@Test
@TestMetadata("safeCallChainMemberExt2.kt")
public void testSafeCallChainMemberExt2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChainMemberExt2.kt");
}
}
@Nested
@TestMetadata("compiler/testData/codegen/bytecodeText/inline")
@TestDataPath("$PROJECT_ROOT")
@@ -725,9 +725,21 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
}
@Test
@TestMetadata("safeCallToPrimitiveEquality.kt")
public void testSafeCallToPrimitiveEquality() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality.kt");
@TestMetadata("safeCallToPrimitiveEquality1.kt")
public void testSafeCallToPrimitiveEquality1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality1.kt");
}
@Test
@TestMetadata("safeCallToPrimitiveEquality2.kt")
public void testSafeCallToPrimitiveEquality2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality2.kt");
}
@Test
@TestMetadata("safeCallToPrimitiveEquality3.kt")
public void testSafeCallToPrimitiveEquality3() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/boxingOptimization/safeCallToPrimitiveEquality3.kt");
}
@Test
@@ -3231,6 +3243,40 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest {
}
}
@Nested
@TestMetadata("compiler/testData/codegen/bytecodeText/ifNullChain")
@TestDataPath("$PROJECT_ROOT")
public class IfNullChain {
@Test
public void testAllFilesPresentInIfNullChain() throws Exception {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("compiler/testData/codegen/bytecodeText/ifNullChain"), Pattern.compile("^(.+)\\.kt$"), null, TargetBackend.JVM_IR, true);
}
@Test
@TestMetadata("safeCallChain1.kt")
public void testSafeCallChain1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChain1.kt");
}
@Test
@TestMetadata("safeCallChain2.kt")
public void testSafeCallChain2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChain2.kt");
}
@Test
@TestMetadata("safeCallChainMemberExt1.kt")
public void testSafeCallChainMemberExt1() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChainMemberExt1.kt");
}
@Test
@TestMetadata("safeCallChainMemberExt2.kt")
public void testSafeCallChainMemberExt2() throws Exception {
runTest("compiler/testData/codegen/bytecodeText/ifNullChain/safeCallChainMemberExt2.kt");
}
}
@Nested
@TestMetadata("compiler/testData/codegen/bytecodeText/inline")
@TestDataPath("$PROJECT_ROOT")