diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/when/SwitchCodegen.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/when/SwitchCodegen.kt index cc78eb8a6cf..2169f30a78d 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/when/SwitchCodegen.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/when/SwitchCodegen.kt @@ -186,12 +186,7 @@ abstract class SwitchCodegen( val minValue = keys.first() val rangeLength = maxValue.toLong() - minValue.toLong() + 1L - // In modern JVM implementations it shouldn't matter very much for runtime performance - // whether to choose lookupswitch or tableswitch. - // The only metric that really matters is bytecode size and here we can estimate: - // - lookupswitch: ~ 2 * labelsNumber - // - tableswitch: ~ rangeLength - if (rangeLength > 2L * labelsNumber || rangeLength > Int.MAX_VALUE) { + if (preferLookupOverSwitch(labelsNumber, rangeLength)) { val labels = transitionsTable.values.toTypedArray() v.lookupswitch(defaultLabel, keys, labels) return @@ -219,4 +214,13 @@ abstract class SwitchCodegen( } } } + + companion object { + // In modern JVM implementations it shouldn't matter very much for runtime performance + // whether to choose lookupswitch or tableswitch. + // The only metric that really matters is bytecode size and here we can estimate: + // - lookupswitch: ~ 2 * labelsNumber + // - tableswitch: ~ rangeLength + fun preferLookupOverSwitch(labelsNumber: Int, rangeLength: Long) = rangeLength > 2L * labelsNumber || rangeLength > Int.MAX_VALUE + } } diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt index edfbb70991d..a363760a299 100644 --- a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/ExpressionCodegen.kt @@ -688,7 +688,8 @@ class ExpressionCodegen( override fun visitWhen(expression: IrWhen, data: BlockInfo): StackValue { expression.markLineNumber(startOffset = true) - return genIfWithBranches(expression.branches[0], data, expression.type.toKotlinType(), expression.branches.drop(1)) + val switch = SwitchGenerator(expression, data, this).generate() + return switch ?: genIfWithBranches(expression.branches[0], data, expression.type.toKotlinType(), expression.branches.drop(1)) } private fun genIfWithBranches(branch: IrBranch, data: BlockInfo, type: KotlinType, otherBranches: List): StackValue { @@ -1204,7 +1205,7 @@ class ExpressionCodegen( } - private fun coerceNotToUnit(fromType: Type, fromKotlinType: KotlinType?, toKotlinType: KotlinType): StackValue { + internal fun coerceNotToUnit(fromType: Type, fromKotlinType: KotlinType?, toKotlinType: KotlinType): StackValue { val asmToType = toKotlinType.asmType if (asmToType != AsmTypes.UNIT_TYPE || TypeUtils.isNullableType(toKotlinType)) { coerce(fromType, fromKotlinType, asmToType, toKotlinType, mv) diff --git a/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/SwitchGenerator.kt b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/SwitchGenerator.kt new file mode 100644 index 00000000000..543d8d5a12e --- /dev/null +++ b/compiler/ir/backend.jvm/src/org/jetbrains/kotlin/backend/jvm/codegen/SwitchGenerator.kt @@ -0,0 +1,194 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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.jvm.codegen + +import org.jetbrains.kotlin.codegen.* +import org.jetbrains.kotlin.codegen.StackValue.* +import org.jetbrains.kotlin.codegen.`when`.SwitchCodegen.Companion.preferLookupOverSwitch +import org.jetbrains.kotlin.ir.IrElement +import org.jetbrains.kotlin.ir.expressions.* +import org.jetbrains.kotlin.ir.types.* +import org.jetbrains.kotlin.ir.util.dump +import org.jetbrains.kotlin.ir.util.isTrueConst +import org.jetbrains.kotlin.types.KotlinType +import org.jetbrains.org.objectweb.asm.Label +import org.jetbrains.org.objectweb.asm.Type +import java.util.* + +// TODO: eliminate the temporary variable +class SwitchGenerator(private val expression: IrWhen, private val data: BlockInfo, private val codegen: ExpressionCodegen) { + private val mv = codegen.mv + + // @return null if the IrWhen cannot be emitted as lookupswitch or tableswitch. + fun generate(): StackValue? { + val endLabel = Label() + var defaultLabel = endLabel + val thenExpressions = ArrayList>() + var elseExpression: IrExpression? = null + val allConditions = ArrayList>() + + // Parse the when structure. Note that the condition can be nested. See matchConditions() for details. + for (branch in expression.branches) { + if (branch is IrElseBranch) { + elseExpression = branch.result + defaultLabel = Label() + } else { + val conditions = matchConditions(branch.condition) ?: return null + val thenLabel = Label() + thenExpressions.add(Pair(branch.result, thenLabel)) + allConditions += conditions.map { Pair(it, thenLabel) } + } + } + + // IF is more compact when there are only 1 or fewer branches, in addition to else. + if (allConditions.size <= 1) + return null + + if (areConstIntComparisons(allConditions.map { it.first })) { + // if all conditions are CALL EQEQ(tmp_variable, some_int_constant) + val cases = allConditions.mapTo(ArrayList()) { Pair((it.first.getValueArgument(1) as IrConst<*>).value as Int, it.second) } + val subject = allConditions[0].first.getValueArgument(0)!! as IrGetValue + return gen(cases, subject, defaultLabel, endLabel, elseExpression, thenExpressions) + } + + // TODO: String, Enum, etc. + return null + } + + // A lookup/table switch can be used if... + private fun areConstIntComparisons(conditions: List): Boolean { + // 1. All branches are CALL 'EQEQ(Any?, Any?)': Boolean + if (conditions.any { it.symbol != codegen.classCodegen.context.irBuiltIns.eqeqSymbol }) + return false + + // 2. All types of variables involved in comparison are Int. + // 3. All arg0 refer to the same value. + val lhs = conditions.map { it.getValueArgument(0) as? IrGetValue } + if (lhs.any { it == null || it.symbol != lhs[0]!!.symbol || !it.type.isInt() }) + return false + + // 4. All arg1 are IrConst<*>. + val rhs = conditions.map { it.getValueArgument(1) as? IrConst<*> } + if (rhs.any { it == null || it.kind != IrConstKind.Int }) + return false + + return true + } + + // psi2ir lowers multiple cases to nested conditions. For example, + // + // when (subject) { + // a, b, c -> action + // } + // + // is lowered to + // + // if (if (subject == a) + // true + // else + // if (subject == b) + // true + // else + // subject == c) { + // action + // } + // + // @return true if the conditions are equality checks of constants. + private fun matchConditions(condition: IrExpression): ArrayList? { + if (condition is IrCall) { + return arrayListOf(condition) + } else if (condition is IrWhen && condition.origin == IrStatementOrigin.WHEN_COMMA) { + assert(condition.type.isBoolean()) { "WHEN_COMMA should always be a Boolean: ${condition.dump()}" } + + val candidates = ArrayList() + + // Match the following structure: + // + // when() { + // cond_1 -> true + // cond_2 -> true + // ... + // else -> cond_N + // } + // + // Namely, the structure which returns true if any one of the condition is true. + for (branch in condition.branches) { + if (branch is IrElseBranch) { + assert(branch.condition.isTrueConst()) { "IrElseBranch.condition should be const true: ${branch.condition.dump()}" } + candidates += matchConditions(branch.result) ?: return null + } else { + if (!branch.result.isTrueConst()) + return null + candidates += matchConditions(branch.condition) ?: return null + } + } + + return if (candidates.isNotEmpty()) candidates else return null + } + + return null + } + + private fun gen(expression: IrElement, data: BlockInfo): StackValue = codegen.gen(expression, data) + + private fun coerceNotToUnit(fromType: Type, fromKotlinType: KotlinType?, toKotlinType: KotlinType): StackValue = + codegen.coerceNotToUnit(fromType, fromKotlinType, toKotlinType) + + private fun gen( + cases: ArrayList>, + subject: IrGetValue, + defaultLabel: Label, + endLabel: Label, + elseExpression: IrExpression?, + thenExpressions: ArrayList> + ): StackValue { + cases.sortBy { it.first } + + // Emit the temporary variable for subject. + gen(subject, data) + + val caseMin = cases.first().first + val caseMax = cases.last().first + val rangeLength = caseMax - caseMin + 1L + + // Emit either tableswitch or lookupswitch, depending on the code size. + // + // lookupswitch is 2X as large as tableswitch with the same entries. However, lookupswitch is sparse while tableswitch must + // enumerate all the entries in the range. + if (preferLookupOverSwitch(cases.size, rangeLength)) { + mv.lookupswitch(defaultLabel, cases.map { it.first }.toIntArray(), cases.map { it.second }.toTypedArray()) + } else { + val labels = Array(rangeLength.toInt()) { defaultLabel } + for (case in cases) + labels[case.first - caseMin] = case.second + mv.tableswitch(caseMin, caseMax, defaultLabel, *labels) + } + + // all entries except else + for (thenExpression in thenExpressions) { + mv.visitLabel(thenExpression.second) + val stackValue = thenExpression.first.run { gen(this, data) } + coerceNotToUnit(stackValue.type, stackValue.kotlinType, expression.type.toKotlinType()) + mv.goTo(endLabel) + } + + // else + val result = if (elseExpression == null) { + // There's no else part. No stack value will be generated. + StackValue.putUnitInstance(mv) + onStack(Type.VOID_TYPE) + } else { + // Generate the else part. + mv.visitLabel(defaultLabel) + val stackValue = elseExpression.run { gen(this, data) } + coerceNotToUnit(stackValue.type, stackValue.kotlinType, expression.type.toKotlinType()) + } + + mv.mark(endLabel) + return result + } +} + diff --git a/compiler/testData/codegen/bytecodeText/when/inlineConstValsInsideWhen.kt b/compiler/testData/codegen/bytecodeText/when/inlineConstValsInsideWhen.kt index 368e6e40882..828e99cb3d8 100644 --- a/compiler/testData/codegen/bytecodeText/when/inlineConstValsInsideWhen.kt +++ b/compiler/testData/codegen/bytecodeText/when/inlineConstValsInsideWhen.kt @@ -1,4 +1,3 @@ -// IGNORE_BACKEND: JVM_IR const val A = 10 private const val B = 20 diff --git a/compiler/testData/codegen/bytecodeText/when/lookupSwitch.kt b/compiler/testData/codegen/bytecodeText/when/lookupSwitch.kt new file mode 100644 index 00000000000..d0d154ce0b5 --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/when/lookupSwitch.kt @@ -0,0 +1,11 @@ +fun foo(x: Int): String { + return when (x) { + 100 -> "1" + 200 -> "2" + 300 -> "3" + else -> "else" + } +} + +// 1 LOOKUPSWITCH +// 0 TABLESWITCH \ No newline at end of file diff --git a/compiler/testData/codegen/bytecodeText/when/qualifiedConstValsInsideWhen.kt b/compiler/testData/codegen/bytecodeText/when/qualifiedConstValsInsideWhen.kt index 20fd74ac9f5..44cf346d69f 100644 --- a/compiler/testData/codegen/bytecodeText/when/qualifiedConstValsInsideWhen.kt +++ b/compiler/testData/codegen/bytecodeText/when/qualifiedConstValsInsideWhen.kt @@ -1,4 +1,3 @@ -// IGNORE_BACKEND: JVM_IR object Constants { const val A = 30 const val B = 40 diff --git a/compiler/testData/codegen/bytecodeText/when/simpleConstValsInsideWhen.kt b/compiler/testData/codegen/bytecodeText/when/simpleConstValsInsideWhen.kt index 43cbcc035fd..229da2d1864 100644 --- a/compiler/testData/codegen/bytecodeText/when/simpleConstValsInsideWhen.kt +++ b/compiler/testData/codegen/bytecodeText/when/simpleConstValsInsideWhen.kt @@ -1,4 +1,3 @@ -// IGNORE_BACKEND: JVM_IR const val A = 10 private const val B = 20 diff --git a/compiler/testData/codegen/bytecodeText/when/tableSwitch.kt b/compiler/testData/codegen/bytecodeText/when/tableSwitch.kt new file mode 100644 index 00000000000..7b6e77b90af --- /dev/null +++ b/compiler/testData/codegen/bytecodeText/when/tableSwitch.kt @@ -0,0 +1,11 @@ +fun foo(x: Int): String { + return when (x) { + 101 -> "1" + 102 -> "2" + 103 -> "3" + else -> "else" + } +} + +// 0 LOOKUPSWITCH +// 1 TABLESWITCH \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java index f0280febeca..0239e8afa56 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/BytecodeTextTestGenerated.java @@ -3353,6 +3353,11 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest { runTest("compiler/testData/codegen/bytecodeText/when/kt18818.kt"); } + @TestMetadata("lookupSwitch.kt") + public void testLookupSwitch() throws Exception { + runTest("compiler/testData/codegen/bytecodeText/when/lookupSwitch.kt"); + } + @TestMetadata("noBoxingInDefaultWhenWithSpecialCases.kt") public void testNoBoxingInDefaultWhenWithSpecialCases() throws Exception { runTest("compiler/testData/codegen/bytecodeText/when/noBoxingInDefaultWhenWithSpecialCases.kt"); @@ -3393,6 +3398,11 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest { runTest("compiler/testData/codegen/bytecodeText/when/subjectValInStringWhenHasLocalVariableSlot.kt"); } + @TestMetadata("tableSwitch.kt") + public void testTableSwitch() throws Exception { + runTest("compiler/testData/codegen/bytecodeText/when/tableSwitch.kt"); + } + @TestMetadata("whenNull.kt") public void testWhenNull() throws Exception { runTest("compiler/testData/codegen/bytecodeText/when/whenNull.kt"); diff --git a/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java b/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java index 3f7c3a8ab3f..5d4e1dc771e 100644 --- a/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java +++ b/compiler/tests/org/jetbrains/kotlin/codegen/ir/IrBytecodeTextTestGenerated.java @@ -3353,6 +3353,11 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest { runTest("compiler/testData/codegen/bytecodeText/when/kt18818.kt"); } + @TestMetadata("lookupSwitch.kt") + public void testLookupSwitch() throws Exception { + runTest("compiler/testData/codegen/bytecodeText/when/lookupSwitch.kt"); + } + @TestMetadata("noBoxingInDefaultWhenWithSpecialCases.kt") public void testNoBoxingInDefaultWhenWithSpecialCases() throws Exception { runTest("compiler/testData/codegen/bytecodeText/when/noBoxingInDefaultWhenWithSpecialCases.kt"); @@ -3393,6 +3398,11 @@ public class IrBytecodeTextTestGenerated extends AbstractIrBytecodeTextTest { runTest("compiler/testData/codegen/bytecodeText/when/subjectValInStringWhenHasLocalVariableSlot.kt"); } + @TestMetadata("tableSwitch.kt") + public void testTableSwitch() throws Exception { + runTest("compiler/testData/codegen/bytecodeText/when/tableSwitch.kt"); + } + @TestMetadata("whenNull.kt") public void testWhenNull() throws Exception { runTest("compiler/testData/codegen/bytecodeText/when/whenNull.kt");