Optimize coercion to Unit.

POP operations are backward-propagated.

Operation can't be transformed if its result is moved within stack
(by DUP, DUP_X1, DUP_X2, DUP2, DUP2_X1, DUP2_X2, or SWAP).

Unnecessary operations are replaced with NOPs (for debugger).
Unnecessary NOPs are removed.

KT-9922 Suboptimal generation for simple safe call with unused return value
KT-11116 Optimize methods returning Unit
This commit is contained in:
Dmitry Petrov
2016-04-05 17:27:01 +03:00
parent c4a568efff
commit f1b061d662
12 changed files with 433 additions and 84 deletions
@@ -150,13 +150,13 @@ class LabelNormalizationMethodTransformer : MethodTransformer() {
}
}
private fun InsnList.replaceNodeGetNext(oldNode: AbstractInsnNode, newNode: AbstractInsnNode): AbstractInsnNode? {
fun InsnList.replaceNodeGetNext(oldNode: AbstractInsnNode, newNode: AbstractInsnNode): AbstractInsnNode? {
insertBefore(oldNode, newNode)
remove(oldNode)
return newNode.next
}
private fun InsnList.removeNodeGetNext(oldNode: AbstractInsnNode): AbstractInsnNode? {
fun InsnList.removeNodeGetNext(oldNode: AbstractInsnNode): AbstractInsnNode? {
val next = oldNode.next
remove(oldNode)
return next
@@ -20,6 +20,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.kotlin.codegen.inline.InlineCodegenUtil;
import org.jetbrains.kotlin.codegen.optimization.boxing.RedundantBoxingMethodTransformer;
import org.jetbrains.kotlin.codegen.optimization.boxing.RedundantCoercionToUnitTransformer;
import org.jetbrains.kotlin.codegen.optimization.boxing.RedundantNullCheckMethodTransformer;
import org.jetbrains.kotlin.codegen.optimization.common.UtilKt;
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer;
@@ -42,7 +43,8 @@ public class OptimizationMethodVisitor extends MethodVisitor {
new RedundantNullCheckMethodTransformer(),
new RedundantBoxingMethodTransformer(),
new DeadCodeEliminationMethodTransformer(),
new RedundantGotoMethodTransformer()
new RedundantGotoMethodTransformer(),
new RedundantCoercionToUnitTransformer()
};
private final MethodNode methodNode;
@@ -117,89 +117,86 @@ open class BoxingInterpreter(private val insnList: InsnList) : OptimizationBasic
protected open fun onMergeFail(value: BoxedBasicValue) {}
protected open fun onMergeSuccess(v: BoxedBasicValue, w: BoxedBasicValue) {}
companion object {
private val UNBOXING_METHOD_NAMES =
ImmutableSet.of("booleanValue", "charValue", "byteValue", "shortValue", "intValue", "floatValue", "longValue", "doubleValue")
}
private val KCLASS_TO_JLCLASS = Type.getMethodDescriptor(AsmTypes.JAVA_CLASS_TYPE, AsmTypes.K_CLASS_TYPE)
private val JLCLASS_TO_KCLASS = Type.getMethodDescriptor(AsmTypes.K_CLASS_TYPE, AsmTypes.JAVA_CLASS_TYPE)
private val UNBOXING_METHOD_NAMES =
ImmutableSet.of("booleanValue", "charValue", "byteValue", "shortValue", "intValue", "floatValue", "longValue", "doubleValue")
private fun isWrapperClassNameOrNumber(internalClassName: String) =
isWrapperClassName(internalClassName) || internalClassName == Type.getInternalName(Number::class.java)
private val KCLASS_TO_JLCLASS = Type.getMethodDescriptor(AsmTypes.JAVA_CLASS_TYPE, AsmTypes.K_CLASS_TYPE)
private val JLCLASS_TO_KCLASS = Type.getMethodDescriptor(AsmTypes.K_CLASS_TYPE, AsmTypes.JAVA_CLASS_TYPE)
private fun isWrapperClassName(internalClassName: String) =
JvmPrimitiveType.isWrapperClassName(buildFqNameByInternal(internalClassName))
fun AbstractInsnNode.isUnboxing() =
isPrimitiveUnboxing() || isJavaLangClassUnboxing()
private fun buildFqNameByInternal(internalClassName: String) =
FqName(Type.getObjectType(internalClassName).className)
fun AbstractInsnNode.isBoxing() =
isPrimitiveBoxing() || isJavaLangClassBoxing()
private fun AbstractInsnNode.isUnboxing() =
isPrimitiveUnboxing() || isJavaLangClassUnboxing()
private inline fun AbstractInsnNode.isMethodInsnWith(opcode: Int, condition: MethodInsnNode.() -> Boolean): Boolean =
if (this.opcode == opcode && this is MethodInsnNode)
this.condition()
else
false
private fun AbstractInsnNode.isPrimitiveUnboxing() =
isMethodInsnWith(Opcodes.INVOKEVIRTUAL) {
isWrapperClassNameOrNumber(owner) && isUnboxingMethodName(name)
}
private fun AbstractInsnNode.isJavaLangClassUnboxing() =
isMethodInsnWith(Opcodes.INVOKESTATIC) {
owner == "kotlin/jvm/JvmClassMappingKt" &&
name == "getJavaClass" &&
desc == KCLASS_TO_JLCLASS
}
private fun isUnboxingMethodName(name: String) =
UNBOXING_METHOD_NAMES.contains(name)
private fun AbstractInsnNode.isBoxing() =
this.isPrimitiveBoxing() || this.isJavaLangClassBoxing()
private fun AbstractInsnNode.isPrimitiveBoxing() =
isMethodInsnWith(Opcodes.INVOKESTATIC) {
isWrapperClassName(owner) &&
name == "valueOf" &&
isBoxingMethodDescriptor()
}
private fun MethodInsnNode.isBoxingMethodDescriptor(): Boolean {
val ownerType = Type.getObjectType(owner)
return desc == Type.getMethodDescriptor(ownerType, AsmUtil.unboxType(ownerType))
fun AbstractInsnNode.isPrimitiveUnboxing() =
isMethodInsnWith(Opcodes.INVOKEVIRTUAL) {
isWrapperClassNameOrNumber(owner) && isUnboxingMethodName(name)
}
private fun AbstractInsnNode.isJavaLangClassBoxing() =
isMethodInsnWith(Opcodes.INVOKESTATIC) {
owner == AsmTypes.REFLECTION &&
name == "getOrCreateKotlinClass" &&
desc == JLCLASS_TO_KCLASS
}
private fun AbstractInsnNode.isJavaLangClassUnboxing() =
isMethodInsnWith(Opcodes.INVOKESTATIC) {
owner == "kotlin/jvm/JvmClassMappingKt" &&
name == "getJavaClass" &&
desc == KCLASS_TO_JLCLASS
}
private fun AbstractInsnNode.isNextMethodCallOfProgressionIterator(values: List<BasicValue>) =
values[0] is ProgressionIteratorBasicValue &&
isMethodInsnWith(INVOKEINTERFACE) {
name == "next"
}
inline fun AbstractInsnNode.isMethodInsnWith(opcode: Int, condition: MethodInsnNode.() -> Boolean): Boolean =
this.opcode == opcode && this is MethodInsnNode && this.condition()
private fun AbstractInsnNode.isIteratorMethodCallOfProgression(values: List<BasicValue>) =
isMethodInsnWith(INVOKEINTERFACE) {
val firstArgType = values[0].type
firstArgType != null &&
isProgressionClass(firstArgType.internalName) &&
name == "iterator"
}
private fun isWrapperClassNameOrNumber(internalClassName: String) =
isWrapperClassName(internalClassName) || internalClassName == Type.getInternalName(Number::class.java)
private fun isProgressionClass(internalClassName: String) =
RangeCodegenUtil.isRangeOrProgression(buildFqNameByInternal(internalClassName))
private fun isWrapperClassName(internalClassName: String) =
JvmPrimitiveType.isWrapperClassName(buildFqNameByInternal(internalClassName))
private fun getValuesTypeOfProgressionClass(progressionClassInternalName: String) =
RangeCodegenUtil.getPrimitiveRangeOrProgressionElementType(buildFqNameByInternal(progressionClassInternalName))?.let {
type ->
type.typeName.asString()
} ?: error("type should be not null")
}
private fun buildFqNameByInternal(internalClassName: String) =
FqName(Type.getObjectType(internalClassName).className)
private fun isUnboxingMethodName(name: String) =
UNBOXING_METHOD_NAMES.contains(name)
fun AbstractInsnNode.isPrimitiveBoxing() =
isMethodInsnWith(Opcodes.INVOKESTATIC) {
isWrapperClassName(owner) &&
name == "valueOf" &&
isBoxingMethodDescriptor()
}
private fun MethodInsnNode.isBoxingMethodDescriptor(): Boolean {
val ownerType = Type.getObjectType(owner)
return desc == Type.getMethodDescriptor(ownerType, AsmUtil.unboxType(ownerType))
}
private fun AbstractInsnNode.isJavaLangClassBoxing() =
isMethodInsnWith(Opcodes.INVOKESTATIC) {
owner == AsmTypes.REFLECTION &&
name == "getOrCreateKotlinClass" &&
desc == JLCLASS_TO_KCLASS
}
private fun AbstractInsnNode.isNextMethodCallOfProgressionIterator(values: List<BasicValue>) =
values[0] is ProgressionIteratorBasicValue &&
isMethodInsnWith(Opcodes.INVOKEINTERFACE) {
name == "next"
}
private fun AbstractInsnNode.isIteratorMethodCallOfProgression(values: List<BasicValue>) =
isMethodInsnWith(Opcodes.INVOKEINTERFACE) {
val firstArgType = values[0].type
firstArgType != null &&
isProgressionClass(firstArgType.internalName) &&
name == "iterator"
}
private fun isProgressionClass(internalClassName: String) =
RangeCodegenUtil.isRangeOrProgression(buildFqNameByInternal(internalClassName))
private fun getValuesTypeOfProgressionClass(progressionClassInternalName: String) =
RangeCodegenUtil.getPrimitiveRangeOrProgressionElementType(buildFqNameByInternal(progressionClassInternalName))?.let {
type ->
type.typeName.asString()
} ?: error("type should be not null")
@@ -0,0 +1,228 @@
/*
* Copyright 2010-2016 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.boxing
import org.jetbrains.kotlin.codegen.optimization.common.InsnSequence
import org.jetbrains.kotlin.codegen.optimization.common.isMeaningful
import org.jetbrains.kotlin.codegen.optimization.fixStack.peek
import org.jetbrains.kotlin.codegen.optimization.fixStack.top
import org.jetbrains.kotlin.codegen.optimization.removeNodeGetNext
import org.jetbrains.kotlin.codegen.optimization.replaceNodeGetNext
import org.jetbrains.kotlin.codegen.optimization.transformer.MethodTransformer
import org.jetbrains.org.objectweb.asm.Opcodes
import org.jetbrains.org.objectweb.asm.tree.*
import org.jetbrains.org.objectweb.asm.tree.analysis.Analyzer
import org.jetbrains.org.objectweb.asm.tree.analysis.Frame
import org.jetbrains.org.objectweb.asm.tree.analysis.SourceInterpreter
import org.jetbrains.org.objectweb.asm.tree.analysis.SourceValue
import org.jetbrains.org.objectweb.asm.util.Printer
class RedundantCoercionToUnitTransformer : MethodTransformer() {
override fun transform(internalClassName: String, methodNode: MethodNode) {
Transformer(methodNode).transform()
}
private class Transformer(val methodNode: MethodNode) {
private val insnList = methodNode.instructions
private val frames: Array<Frame<SourceValue>?> = Analyzer<SourceValue>(SourceInterpreter()).analyze("fake", methodNode)
private val insns = insnList.toArray()
private val dontTouchInsns = hashSetOf<AbstractInsnNode>()
private val transformations = hashMapOf<AbstractInsnNode, () -> Unit>()
private val removableNops = hashSetOf<InsnNode>()
fun transform() {
computeDontTouchInsns()
computeTransformations()
transformations.values.forEach { it() }
postprocessNops()
}
private fun computeDontTouchInsns() {
for (i in 0..insns.lastIndex) {
val frame = frames[i] ?: continue
val insn = insns[i]
when (insn.opcode) {
Opcodes.DUP ->
dontTouchWordsOnTop(i, frame, 1)
Opcodes.DUP_X1 ->
dontTouchWordsOnTop(i, frame, 2)
Opcodes.DUP_X2 ->
dontTouchWordsOnTop(i, frame, 3)
Opcodes.DUP2 ->
dontTouchWordsOnTop(i, frame, 2)
Opcodes.DUP2_X1 ->
dontTouchWordsOnTop(i, frame, 3)
Opcodes.DUP2_X2 ->
dontTouchWordsOnTop(i, frame, 4)
Opcodes.SWAP ->
dontTouchWordsOnTop(i, frame, 2)
}
}
}
private fun dontTouchWordsOnTop(at: Int, frame: Frame<SourceValue>, expectedWords: Int) {
var words = 0
var offset = 0
while (words < expectedWords) {
val value = frame.peek(offset) ?: throwIllegalStackInsn(at)
offset++
words += value.size
dontTouchInsns.addAll(value.insns)
}
if (words != expectedWords) throwIllegalStackInsn(at)
}
private fun computeTransformations() {
transformations.clear()
for (i in 0..insns.lastIndex) {
if (frames[i] == null) continue
val insn = insns[i]
if (insn.opcode == Opcodes.POP) {
propagatePopBackwards(insn, 0)
}
}
}
private fun propagatePopBackwards(insn: AbstractInsnNode, poppedValueSize: Int) {
if (transformations.containsKey(insn)) return
when {
insn.opcode == Opcodes.POP -> {
val inputTop = getInputTop(insn)
val sources = inputTop.insns
if (sources.all { !isDontTouch(it) } && sources.any { isTransformablePopOperand(it) }) {
transformations[insn] = replaceWithNopTransformation(insn)
sources.forEach { propagatePopBackwards(it, inputTop.size) }
}
}
insn.opcode == Opcodes.CHECKCAST -> {
val inputTop = getInputTop(insn)
val sources = inputTop.insns
val resultType = (insn as TypeInsnNode).desc
if (sources.all { !isDontTouch(it) } && sources.any { isTransformableCheckcastOperand(it, resultType) }) {
transformations[insn] = replaceWithNopTransformation(insn)
sources.forEach { propagatePopBackwards(it, inputTop.size) }
}
else {
transformations[insn] = insertPopAfterTransformation(insn, poppedValueSize)
}
}
insn.isPrimitiveBoxing() -> {
val boxedValueSize = getInputTop(insn).size
transformations[insn] = replaceWithPopTransformation(insn, boxedValueSize)
}
insn.isUnitOrNull() -> {
transformations[insn] = replaceWithNopTransformation(insn)
}
else -> {
transformations[insn] = insertPopAfterTransformation(insn, poppedValueSize)
}
}
}
private fun postprocessNops() {
var node: AbstractInsnNode? = insnList.first
var hasRemovableNops = false
while (node != null) {
node = node.next
val begin = node ?: break
while (node != null && node !is LabelNode) {
if (node in removableNops) {
hasRemovableNops = true
}
node = node.next
}
val end = node
if (hasRemovableNops) {
removeUnneededNopsInRange(begin, end)
}
hasRemovableNops = false
}
}
private fun removeUnneededNopsInRange(begin: AbstractInsnNode, end: AbstractInsnNode?) {
var node: AbstractInsnNode? = begin
var keepNop = true
while (node != null && node != end) {
if (node in removableNops && !keepNop) {
node = insnList.removeNodeGetNext(node)
}
else {
if (node.isMeaningful) keepNop = false
node = node.next
}
}
}
private fun replaceWithPopTransformation(insn: AbstractInsnNode, size: Int): () -> Unit =
{ insnList.replaceNodeGetNext(insn, createPopInsn(size)) }
private fun insertPopAfterTransformation(insn: AbstractInsnNode, size: Int) =
{ insnList.insert(insn, createPopInsn(size)) }
private fun replaceWithNopTransformation(insn: AbstractInsnNode): () -> Unit =
{ insnList.replaceNodeGetNext(insn, createRemovableNopInsn()) }
private fun createPopInsn(size: Int) =
when (size) {
1 -> InsnNode(Opcodes.POP)
2 -> InsnNode(Opcodes.POP2)
else -> throw AssertionError("Unexpected popped value size: $size")
}
private fun createRemovableNopInsn() =
InsnNode(Opcodes.NOP).apply { removableNops.add(this) }
private fun getInputTop(insn: AbstractInsnNode): SourceValue {
val i = insnList.indexOf(insn)
val frame = frames[i] ?: throw AssertionError("Unexpected dead instruction #$i")
return frame.top() ?: throw AssertionError("Instruction #$i has empty stack on input")
}
private fun isTransformableCheckcastOperand(it: AbstractInsnNode, resultType: String) =
it.isPrimitiveBoxing() && (it as MethodInsnNode).owner == resultType
private fun isTransformablePopOperand(insn: AbstractInsnNode) =
insn.opcode == Opcodes.CHECKCAST || insn.isPrimitiveBoxing() || insn.isUnitOrNull()
private fun isDontTouch(insn: AbstractInsnNode) =
insn in dontTouchInsns
private fun throwIllegalStackInsn(i: Int): Nothing =
throw AssertionError("#$i: illegal use of ${Printer.OPCODES[insns[i].opcode]}, input stack: ${formatInputStack(frames[i])}")
private fun formatInputStack(frame: Frame<SourceValue>?): String =
if (frame == null)
"unknown (dead code)"
else
(0..frame.stackSize - 1).map { frame.getStack(it).size }.joinToString(prefix = "[", postfix = "]")
}
}
fun AbstractInsnNode.isUnitOrNull() =
opcode == Opcodes.ACONST_NULL ||
opcode == Opcodes.GETSTATIC && this is FieldInsnNode && owner == "kotlin/Unit" && name == "INSTANCE"
@@ -41,13 +41,11 @@ inline fun InsnList.forEachInlineMarker(block: (String, MethodInsnNode) -> Unit)
}
}
fun <V : Value> Frame<V>.top(): V? {
val stackSize = stackSize
if (stackSize == 0)
return null
else
return getStack(stackSize - 1)
}
fun <V : Value> Frame<V>.top(): V? =
peek(0)
fun <V : Value> Frame<V>.peek(offset: Int): V? =
if (stackSize >= offset) getStack(stackSize - offset - 1) else null
fun MethodNode.updateMaxLocals(newMaxLocals: Int) {
maxLocals = Math.max(maxLocals, newMaxLocals)
@@ -0,0 +1,26 @@
fun test() {
val a = inlineFunInt { 1 }
val b = simpleFunInt { 1 }
val c = inlineFunVoid { val aa = 1 }
val d = simpleFunVoid { val aa = 1 }
}
inline fun inlineFunInt(f: () -> Int): Int {
val a = 1
return f()
}
inline fun inlineFunVoid(f: () -> Unit): Unit {
val a = 1
return f()
}
fun simpleFunInt(f: () -> Int): Int {
return f()
}
fun simpleFunVoid(f: () -> Unit): Unit {
return f()
}
// 3 NOP
@@ -0,0 +1,4 @@
fun test() = Unit
// 0 GETSTATIC
// 0 POP
@@ -0,0 +1,10 @@
interface A {
fun foo()
}
fun test(x: A?) {
x?.foo()
}
// 1 POP
// 0 ACONST_NULL
@@ -0,0 +1,10 @@
interface A {
fun foo(): Any
}
fun test(x: A?) {
x?.foo()
}
// 2 POP
// 0 ACONST_NULL
@@ -0,0 +1,19 @@
fun test(ss: List<String?>) {
val shortStrings = hashSetOf<String>()
val longStrings = hashSetOf<String>()
for (s in ss) {
s?.let {
if (s.length < 4) {
shortStrings.add(s)
}
else {
longStrings.add(s)
}
}
}
}
// 2 POP
// 0 INVOKESTATIC java/lang/Boolean\.valueOf
// 0 CHECKCAST java/lang/Boolean
// 0 ACONST_NULL
@@ -0,0 +1,10 @@
// WITH_RUNTIME
import java.io.*
fun test(r: Reader) {
val ss = hashSetOf<String>()
r.useLines { it.forEach { ss.add(it) } }
}
// 2 POP
@@ -575,6 +575,51 @@ public class BytecodeTextTestGenerated extends AbstractBytecodeTextTest {
}
}
@TestMetadata("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)
public static class CoercionToUnitOptimization extends AbstractBytecodeTextTest {
public void testAllFilesPresentInCoercionToUnitOptimization() throws Exception {
KotlinTestUtils.assertAllTestsPresentByMetadata(this.getClass(), new File("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization"), Pattern.compile("^(.+)\\.kt$"), true);
}
@TestMetadata("nopInlineFuns.kt")
public void testNopInlineFuns() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization/nopInlineFuns.kt");
doTest(fileName);
}
@TestMetadata("returnsUnit.kt")
public void testReturnsUnit() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization/returnsUnit.kt");
doTest(fileName);
}
@TestMetadata("safeCall.kt")
public void testSafeCall() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization/safeCall.kt");
doTest(fileName);
}
@TestMetadata("safeCallWithReturnValue.kt")
public void testSafeCallWithReturnValue() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization/safeCallWithReturnValue.kt");
doTest(fileName);
}
@TestMetadata("safeLet.kt")
public void testSafeLet() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization/safeLet.kt");
doTest(fileName);
}
@TestMetadata("tryInlined.kt")
public void testTryInlined() throws Exception {
String fileName = KotlinTestUtils.navigationMetadata("compiler/testData/codegen/bytecodeText/coercionToUnitOptimization/tryInlined.kt");
doTest(fileName);
}
}
@TestMetadata("compiler/testData/codegen/bytecodeText/conditions")
@TestDataPath("$PROJECT_ROOT")
@RunWith(JUnit3RunnerWithInners.class)