Optimize constant conditions

Using basic constant propagation (only integer constants, no arithmetic
calculations), rewrite conditional jump instructions with constant
arguments.

This covers problem description in KT-17007.
Note that it also works transparently with inline functions.
Partial evaluation is required to cover more "advanced" cases.

As a side effect, this also covers KT-3098:
rewrite IF_ICMP<cmp_op>(x, 0) to IF<cmp0_op>(x).
This commit is contained in:
Dmitry Petrov
2017-05-12 15:22:52 +03:00
parent 495fba43c0
commit 2051355d6a
8 changed files with 372 additions and 6 deletions
@@ -0,0 +1,187 @@
/*
* Copyright 2010-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.codegen.optimization
import org.jetbrains.kotlin.codegen.optimization.common.OptimizationBasicInterpreter
import org.jetbrains.kotlin.codegen.optimization.common.StrictBasicValue
import org.jetbrains.kotlin.codegen.optimization.common.insnText
import org.jetbrains.kotlin.codegen.optimization.fixStack.peek
import org.jetbrains.kotlin.codegen.optimization.fixStack.top
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer
import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.Type
import org.jetbrains.org.objectweb.asm.tree.*
import org.jetbrains.org.objectweb.asm.tree.analysis.BasicValue
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
class ConstantConditionEliminationMethodTransformer : MethodTransformer() {
private val deadCodeElimination = DeadCodeEliminationMethodTransformer()
override fun transform(internalClassName: String, methodNode: MethodNode) {
do {
val changes = ConstantConditionsOptimization(internalClassName, methodNode).run()
if (changes) deadCodeElimination.transform(internalClassName, methodNode)
} while (changes)
}
private class ConstantConditionsOptimization(val internalClassName: String, val methodNode: MethodNode) {
fun run(): Boolean {
val actions = collectRewriteActions()
actions.forEach { it() }
return actions.isNotEmpty()
}
private fun collectRewriteActions(): List<() -> Unit> =
arrayListOf<() -> Unit>().also { actions ->
val frames = analyze(internalClassName, methodNode, ConstantPropagationInterpreter())
val insns = methodNode.instructions.toArray()
for (i in frames.indices) {
val frame = frames[i] ?: continue
val insn = insns[i] as? JumpInsnNode ?: continue
when (insn.opcode) {
in Opcodes.IFEQ .. Opcodes.IFLE ->
tryRewriteComparisonWithZero(insn, frame, actions)
in Opcodes.IF_ICMPEQ .. Opcodes.IF_ICMPLE ->
tryRewriteBinaryComparison(insn, frame, actions)
}
}
}
private fun tryRewriteComparisonWithZero(insn: JumpInsnNode, frame: Frame<BasicValue>, actions: ArrayList<() -> Unit>) {
val top = frame.top()!!.safeAs<IConstValue>() ?: return
val constCondition = when (insn.opcode) {
Opcodes.IFEQ -> top.value == 0
Opcodes.IFNE -> top.value != 0
Opcodes.IFGE -> top.value >= 0
Opcodes.IFGT -> top.value > 0
Opcodes.IFLE -> top.value <= 0
Opcodes.IFLT -> top.value < 0
else -> throw AssertionError("Unexpected instruction: ${insn.insnText}")
}
actions.add {
methodNode.instructions.run {
insertBefore(insn, InsnNode(Opcodes.POP))
if (constCondition)
set(insn, JumpInsnNode(Opcodes.GOTO, insn.label))
else
remove(insn)
}
}
}
private fun tryRewriteBinaryComparison(insn: JumpInsnNode, frame: Frame<BasicValue>, actions: ArrayList<() -> Unit>) {
val arg1 = frame.peek(1)!!
val arg2 = frame.peek(0)!!
if (arg1 is IConstValue && arg2 is IConstValue) {
rewriteBinaryComparisonOfConsts(insn, arg1.value, arg2.value, actions)
}
else if (arg2 is IConstValue && arg2.value == 0) {
rewriteBinaryComparisonWith0(insn, actions)
}
}
private fun rewriteBinaryComparisonOfConsts(insn: JumpInsnNode, value1: Int, value2: Int, actions: ArrayList<() -> Unit>) {
val constCondition = when (insn.opcode) {
Opcodes.IF_ICMPEQ -> value1 == value2
Opcodes.IF_ICMPNE -> value1 != value2
Opcodes.IF_ICMPLE -> value1 <= value2
Opcodes.IF_ICMPLT -> value1 < value2
Opcodes.IF_ICMPGE -> value1 >= value2
Opcodes.IF_ICMPGT -> value1 > value2
else -> throw AssertionError("Unexpected instruction: ${insn.insnText}")
}
actions.add {
methodNode.instructions.run {
insertBefore(insn, InsnNode(Opcodes.POP))
insertBefore(insn, InsnNode(Opcodes.POP))
if (constCondition)
set(insn, JumpInsnNode(Opcodes.GOTO, insn.label))
else
remove(insn)
}
}
}
private fun rewriteBinaryComparisonWith0(insn: JumpInsnNode, actions: ArrayList<() -> Unit>) {
actions.add {
methodNode.instructions.run {
insertBefore(insn, InsnNode(Opcodes.POP))
val cmpWith0Opcode = when (insn.opcode) {
Opcodes.IF_ICMPEQ -> Opcodes.IFEQ
Opcodes.IF_ICMPNE -> Opcodes.IFNE
Opcodes.IF_ICMPLE -> Opcodes.IFLE
Opcodes.IF_ICMPLT -> Opcodes.IFLT
Opcodes.IF_ICMPGE -> Opcodes.IFGE
Opcodes.IF_ICMPGT -> Opcodes.IFGT
else -> throw AssertionError("Unexpected instruction: ${insn.insnText}")
}
set(insn, JumpInsnNode(cmpWith0Opcode, insn.label))
}
}
}
}
private class IConstValue private constructor(val value: Int) : StrictBasicValue(Type.INT_TYPE) {
override fun equals(other: Any?): Boolean =
other === this ||
other is IConstValue && other.value == this.value
override fun hashCode(): Int = value
override fun toString(): String = "IConst($value)"
companion object {
private val ICONST_CACHE = Array(7) { IConstValue(it - 1) }
fun of(value: Int) =
if (value in -1 .. 5)
ICONST_CACHE[value + 1]
else
IConstValue(value)
}
}
private class ConstantPropagationInterpreter : OptimizationBasicInterpreter() {
override fun newOperation(insn: AbstractInsnNode): BasicValue =
when (insn.opcode) {
in Opcodes.ICONST_M1 .. Opcodes.ICONST_5 ->
IConstValue.of(insn.opcode - Opcodes.ICONST_0)
Opcodes.BIPUSH, Opcodes.SIPUSH ->
IConstValue.of(insn.cast<IntInsnNode>().operand)
Opcodes.LDC -> {
val operand = insn.cast<LdcInsnNode>().cst
if (operand is Int)
IConstValue.of(operand)
else
super.newOperation(insn)
}
else -> super.newOperation(insn)
}
override fun merge(v: BasicValue, w: BasicValue): BasicValue =
if (v is IConstValue && w is IConstValue && v == w)
v
else
super.merge(v, w)
}
}
@@ -52,6 +52,7 @@ class OptimizationMethodVisitor(
CapturedVarsOptimizationMethodTransformer(),
RedundantNullCheckMethodTransformer(),
RedundantCheckCastEliminationMethodTransformer(),
ConstantConditionEliminationMethodTransformer(),
RedundantBoxingMethodTransformer(),
RedundantCoercionToUnitTransformer(),
DeadCodeEliminationMethodTransformer(),
@@ -16,6 +16,7 @@
package org.jetbrains.kotlin.codegen.optimization.boxing
import org.jetbrains.kotlin.codegen.optimization.common.isLoadOperation
import org.jetbrains.kotlin.codegen.optimization.common.isMeaningful
import org.jetbrains.kotlin.codegen.optimization.fixStack.top
import org.jetbrains.kotlin.codegen.optimization.removeNodeGetNext
@@ -63,7 +64,7 @@ class RedundantCoercionToUnitTransformer : MethodTransformer() {
private val frames by lazy { analyzeMethodBody() }
fun transform() {
if (!insns.any { it.isUnitInstanceOrNull() }) return
if (!insns.any { it.isPurePush() }) return
computeTransformations()
for ((insn, transformation) in transformations.entries) {
@@ -87,7 +88,7 @@ class RedundantCoercionToUnitTransformer : MethodTransformer() {
}
override fun unaryOperation(insn: AbstractInsnNode, value: SourceValue): SourceValue {
if (insn.opcode != Opcodes.CHECKCAST) {
if (insn.opcode != Opcodes.CHECKCAST && !insn.isPrimitiveTypeConversion()) {
value.insns.markAsDontTouch()
}
return super.unaryOperation(insn, value)
@@ -158,10 +159,22 @@ class RedundantCoercionToUnitTransformer : MethodTransformer() {
transformations[insn] = replaceWithPopTransformation(boxedValueSize)
}
insn.isUnitInstanceOrNull() -> {
insn.isPurePush() -> {
transformations[insn] = replaceWithNopTransformation()
}
insn.isPrimitiveTypeConversion() -> {
val inputTop = getInputTop(insn)
val sources = inputTop.insns
if (sources.all { !isDontTouch(it) }) {
transformations[insn] = replaceWithNopTransformation()
sources.forEach { propagatePopBackwards(it, inputTop.size) }
}
else {
transformations[insn] = replaceWithPopTransformation(poppedValueSize)
}
}
else -> {
transformations[insn] = insertPopAfterTransformation(poppedValueSize)
}
@@ -232,7 +245,7 @@ class RedundantCoercionToUnitTransformer : MethodTransformer() {
it.isPrimitiveBoxing() && (it as MethodInsnNode).owner == resultType
private fun isTransformablePopOperand(insn: AbstractInsnNode) =
insn.opcode == Opcodes.CHECKCAST || insn.isPrimitiveBoxing() || insn.isUnitInstanceOrNull()
insn.opcode == Opcodes.CHECKCAST || insn.isPrimitiveBoxing() || insn.isPurePush()
private fun isDontTouch(insn: AbstractInsnNode) =
dontTouchInsnIndices[insnList.indexOf(insn)]
@@ -240,9 +253,14 @@ class RedundantCoercionToUnitTransformer : MethodTransformer() {
}
fun AbstractInsnNode.isUnitInstanceOrNull() =
opcode == Opcodes.ACONST_NULL || isUnitInstance()
fun AbstractInsnNode.isPurePush() =
isLoadOperation() ||
opcode in Opcodes.ACONST_NULL .. Opcodes.LDC + 2 ||
isUnitInstance()
fun AbstractInsnNode.isUnitInstance() =
opcode == Opcodes.GETSTATIC &&
this is FieldInsnNode && owner == "kotlin/Unit" && name == "INSTANCE"
fun AbstractInsnNode.isPrimitiveTypeConversion() =
opcode in Opcodes.I2L .. Opcodes.I2S
@@ -0,0 +1,25 @@
// FILE: util.kt
inline fun ieq(x: Int, y: Int) = x == y
inline fun ine(x: Int, y: Int) = x != y
inline fun ilt(x: Int, y: Int) = x < y
inline fun ile(x: Int, y: Int) = x <= y
inline fun igt(x: Int, y: Int) = x > y
inline fun ige(x: Int, y: Int) = x >= y
// FILE: test.kt
fun testeq(x: Int) = ieq(x, 0)
fun testne(x: Int) = ine(x, 0)
fun testlt(x: Int) = ilt(x, 0)
fun testle(x: Int) = ile(x, 0)
fun testgt(x: Int) = igt(x, 0)
fun testge(x: Int) = ige(x, 0)
// @TestKt.class:
// 0 IF_ICMPEQ
// 0 IF_ICMPNE
// 0 IF_ICMPGE
// 0 IF_ICMPGT
// 0 IF_ICMPLE
// 0 IF_ICMPLT
@@ -0,0 +1,35 @@
// FILE: util.kt
const val FLAG = true
const val OTHER_FLAG = false
fun doStuff1() {}
fun doStuff2() {}
fun doStuff3() {}
inline fun doStuff1IfTrue(flag: Boolean) {
if (flag) doStuff1()
}
inline fun doStuff2IfFalse(flag: Boolean) {
if (!flag) doStuff2()
}
inline fun doStuff3IfComplex(flag: Boolean) {
if (flag && !OTHER_FLAG) doStuff3()
}
// FILE: test.kt
fun test() {
doStuff1IfTrue(FLAG)
doStuff2IfFalse(FLAG)
doStuff3IfComplex(FLAG)
}
// @TestKt.class:
// 0 FLAG
// 1 INVOKESTATIC UtilKt.doStuff1
// 0 INVOKESTATIC UtilKt.doStuff2
// 1 INVOKESTATIC UtilKt.doStuff3
// 0 ILOAD 0
// 0 IFEQ
// 0 IFNE
@@ -0,0 +1,58 @@
// FILE: util.kt
const val MAGIC = 42
fun doStuffEq() {}
fun doStuffNotEq() {}
fun doStuffLt() {}
fun doStuffLe() {}
fun doStuffGt() {}
fun doStuffGe() {}
inline fun doStuffIfEq(i: Int) {
if (i == MAGIC) doStuffEq()
}
inline fun doStuffIfNotEq(i: Int) {
if (i != MAGIC) doStuffNotEq()
}
inline fun doStuffIfLt(i: Int) {
if (i < MAGIC) doStuffLt()
}
inline fun doStuffIfLe(i: Int) {
if (i <= MAGIC) doStuffLe()
}
inline fun doStuffIfGt(i: Int) {
if (i > MAGIC) doStuffGt()
}
inline fun doStuffIfGe(i: Int) {
if (i >= MAGIC) doStuffGe()
}
// FILE: test.kt
fun test() {
doStuffIfEq(100)
doStuffIfNotEq(100)
doStuffIfLt(100)
doStuffIfLe(100)
doStuffIfGt(100)
doStuffIfGe(100)
}
// @TestKt.class:
// 0 INVOKESTATIC UtilKt.doStuffEq
// 1 INVOKESTATIC UtilKt.doStuffNotEq
// 0 INVOKESTATIC UtilKt.doStuffLt
// 0 INVOKESTATIC UtilKt.doStuffLe
// 1 INVOKESTATIC UtilKt.doStuffGt
// 1 INVOKESTATIC UtilKt.doStuffGe
// 0 ILOAD 0
// 0 IFEQ
// 0 IFNE
// 0 IFLT
// 0 IFLE
// 0 IFGE
// 0 IFGT
@@ -0,0 +1,9 @@
fun testLt(x: String, y: String) = x < y
fun testLe(x: String, y: String) = x <= y
fun testGt(x: String, y: String) = x > y
fun testGe(x: String, y: String) = x >= y
// 0 IF_ICMPGE
// 0 IF_ICMPGT
// 0 IF_ICMPLE
// 0 IF_ICMPLT
@@ -885,6 +885,39 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
}
}
@TestMetadata("compiler/testData/codegen/bytecodeText/constantConditions")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class ConstantConditions extends AbstractBytecodeTextTest {
public void testAllFilesPresentInConstantConditions() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/bytecodeText/constantConditions"), Pattern.compile("^(.+)\\.kt$"), TargetBackend.ANY, true);
}
@TestMetadata("cmpIntWith0.kt")
public void testCmpIntWith0() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/constantConditions/cmpIntWith0.kt");
doTest(fileName);
}
@TestMetadata("constantFlag.kt")
public void testConstantFlag() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/constantConditions/constantFlag.kt");
doTest(fileName);
}
@TestMetadata("constantInt.kt")
public void testConstantInt() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/constantConditions/constantInt.kt");
doTest(fileName);
}
@TestMetadata("kt3098.kt")
public void testKt3098() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/constantConditions/kt3098.kt");
doTest(fileName);
}
}
@TestMetadata("compiler/testData/codegen/bytecodeText/constants")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)