when: emit lookupswitch/tableswitch if possible
A lookupswitch or tableswitch can be used if all conditions are equality
checks to constants. To be more specific, it can be done if:
1. All conditions are CALL 'EQEQ(Any?, Any?)': Boolean
2. All types of variables involved in comparison are in the same group
of Char/Byte/Short/Int, String or enum.
3. All arg0 refer to the same value.
4. All arg1 are IrConst<*>.
Change-Id: Ifd7cb618395f6c5cc64601018b446f0bb7f5891c
This commit is contained in:
committed by
max-kammerer
parent
f5d0a38629
commit
c1d721a15f
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -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<IrBranch>): 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)
|
||||
|
||||
+194
@@ -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<Pair<IrExpression, Label>>()
|
||||
var elseExpression: IrExpression? = null
|
||||
val allConditions = ArrayList<Pair<IrCall, Label>>()
|
||||
|
||||
// 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<IrCall>): 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<IrCall>? {
|
||||
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<IrCall>()
|
||||
|
||||
// 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<Pair<Int, Label>>,
|
||||
subject: IrGetValue,
|
||||
defaultLabel: Label,
|
||||
endLabel: Label,
|
||||
elseExpression: IrExpression?,
|
||||
thenExpressions: ArrayList<Pair<IrExpression, Label>>
|
||||
): 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// IGNORE_BACKEND: JVM_IR
|
||||
const val A = 10
|
||||
private const val B = 20
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fun foo(x: Int): String {
|
||||
return when (x) {
|
||||
100 -> "1"
|
||||
200 -> "2"
|
||||
300 -> "3"
|
||||
else -> "else"
|
||||
}
|
||||
}
|
||||
|
||||
// 1 LOOKUPSWITCH
|
||||
// 0 TABLESWITCH
|
||||
@@ -1,4 +1,3 @@
|
||||
// IGNORE_BACKEND: JVM_IR
|
||||
object Constants {
|
||||
const val A = 30
|
||||
const val B = 40
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// IGNORE_BACKEND: JVM_IR
|
||||
const val A = 10
|
||||
private const val B = 20
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fun foo(x: Int): String {
|
||||
return when (x) {
|
||||
101 -> "1"
|
||||
102 -> "2"
|
||||
103 -> "3"
|
||||
else -> "else"
|
||||
}
|
||||
}
|
||||
|
||||
// 0 LOOKUPSWITCH
|
||||
// 1 TABLESWITCH
|
||||
@@ -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");
|
||||
|
||||
+10
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user