From 8cd1f9566db60f06ab368fad32d474397ece90be Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Thu, 29 Oct 2015 09:48:23 +0100 Subject: Track aliases of primitive values in AliasingFrame This allows using an AliasingAnalyzer for copy propagation (subsequent commit). --- .../nsc/backend/jvm/analysis/AliasingFrame.scala | 39 +++++++------- .../scala/tools/nsc/backend/jvm/CodeGenTools.scala | 2 +- .../tools/nsc/backend/jvm/opt/AnalyzerTest.scala | 63 ++++++++++++++++++++++ 3 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala index 596ee55290..5aa060e019 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala @@ -99,7 +99,7 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc super.execute(insn, interpreter) (insn.getOpcode: @switch) match { - case ALOAD => + case ILOAD | LLOAD | FLOAD | DLOAD | ALOAD => newAlias(assignee = stackTop, source = insn.asInstanceOf[VarInsnNode].`var`) case DUP => @@ -212,23 +212,26 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc } case opcode => - if (opcode == ASTORE) { - // not a separate case: we re-use the code below that removes the consumed stack value from alias sets - val stackTopBefore = stackTop - produced + consumed - val local = insn.asInstanceOf[VarInsnNode].`var` - newAlias(assignee = local, source = stackTopBefore) - // if the value written is size 2, it overwrites the subsequent slot, which is then no - // longer an alias of anything. see the corresponding case in `Frame.execute`. - if (getLocal(local).getSize == 2) - removeAlias(local + 1) - - // if the value at the preceding index is size 2, it is no longer valid, so we remove its - // aliasing. see corresponding case in `Frame.execute` - if (local > 0) { - val precedingValue = getLocal(local - 1) - if (precedingValue != null && precedingValue.getSize == 2) - removeAlias(local - 1) - } + (opcode: @switch) match { + case ISTORE | LSTORE | FSTORE | DSTORE | ASTORE => + // not a separate case: we re-use the code below that removes the consumed stack value from alias sets + val stackTopBefore = stackTop - produced + consumed + val local = insn.asInstanceOf[VarInsnNode].`var` + newAlias(assignee = local, source = stackTopBefore) + // if the value written is size 2, it overwrites the subsequent slot, which is then no + // longer an alias of anything. see the corresponding case in `Frame.execute`. + if (getLocal(local).getSize == 2) + removeAlias(local + 1) + + // if the value at the preceding index is size 2, it is no longer valid, so we remove its + // aliasing. see corresponding case in `Frame.execute` + if (local > 0) { + val precedingValue = getLocal(local - 1) + if (precedingValue != null && precedingValue.getSize == 2) + removeAlias(local - 1) + } + + case _ => } // Remove consumed stack values from aliasing sets. diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index 1f2ec274d3..c071745788 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -179,7 +179,7 @@ object CodeGenTools { def findInstr(method: MethodNode, query: String): List[AbstractInsnNode] = { val useNext = query(0) == '+' val instrPart = if (useNext) query.drop(1) else query - val insns = method.instructions.iterator.asScala.find(i => textify(i) contains instrPart).toList + val insns = method.instructions.iterator.asScala.filter(i => textify(i) contains instrPart).toList if (useNext) insns.map(_.getNext) else insns } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala new file mode 100644 index 0000000000..11014f5e64 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala @@ -0,0 +1,63 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.analysis.{AliasingFrame, AliasingAnalyzer} + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import BackendReporting._ +import BytecodeUtils._ + +import scala.collection.convert.decorateAsScala._ +import scala.tools.testing.ClearAfterClass + +object AnalyzerTest extends ClearAfterClass.Clearable { + var noOptCompiler = newCompiler(extraArgs = "-Yopt:l:none") + def clear(): Unit = { noOptCompiler = null } +} + +@RunWith(classOf[JUnit4]) +class AnalyzerTest extends ClearAfterClass { + ClearAfterClass.stateToClear = AnalyzerTest + val noOptCompiler = AnalyzerTest.noOptCompiler + + @Test + def aliasingOfPrimitives(): Unit = { + val code = + """class C { + | def f(a: Int, b: Long) = { + | val c = a - b // a is converted with i2l + | val d = c + | val e = a + | // locals: 0 this -- 1 a -- 2-3 b -- 4-5 c -- 6-7 d -- 8 e + | e + d // e is converted with i2l + | } + |} + """.stripMargin + + val List(c) = compileClasses(noOptCompiler)(code) + val a = new AliasingAnalyzer(new BasicInterpreter) + val f = findAsmMethod(c, "f") + a.analyze("C", f) + + val List(_, i2l) = findInstr(f, "I2L") + val aliasesAtI2l = a.frameAt(i2l, f).asInstanceOf[AliasingFrame[_]].aliases + assertEquals(aliasesAtI2l(1).iterator.toList, List(1, 8, 9)) // a, e and stack top + assertEquals(aliasesAtI2l(4).iterator.toList, List(4, 6)) + + val List(add) = findInstr(f, "LADD") + val aliasesAtAdd = a.frameAt(add, f).asInstanceOf[AliasingFrame[_]].aliases + assertEquals(aliasesAtAdd(1).iterator.toList, List(1, 8)) // after i2l the value on the stack is no longer an alias + assertEquals(aliasesAtAdd(4).iterator.toList, List(4, 6, 10)) // c, d and stack top + } +} -- cgit v1.2.3 From 6ca9490b179533169da041e9af937f89485e05a1 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Thu, 29 Oct 2015 09:59:51 +0100 Subject: [trivial] import Opcodes._ in two files, move a utility method --- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 84 ++++++++++++---------- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 20 ++---- 2 files changed, 51 insertions(+), 53 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index e543a8c3e0..c6f88a4c98 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -12,7 +12,8 @@ import scala.collection.mutable import scala.reflect.internal.util.Collections._ import scala.tools.asm.commons.CodeSizeEvaluator import scala.tools.asm.tree.analysis._ -import scala.tools.asm.{MethodWriter, ClassWriter, Label, Opcodes, Type} +import scala.tools.asm.{Label, Type} +import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import GenBCode._ import scala.collection.convert.decorateAsScala._ @@ -27,7 +28,7 @@ object BytecodeUtils { object Goto { def unapply(instruction: AbstractInsnNode): Option[JumpInsnNode] = { - if (instruction.getOpcode == Opcodes.GOTO) Some(instruction.asInstanceOf[JumpInsnNode]) + if (instruction.getOpcode == GOTO) Some(instruction.asInstanceOf[JumpInsnNode]) else None } } @@ -57,27 +58,27 @@ object BytecodeUtils { def isJumpNonJsr(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode // JSR is deprecated in classfile version 50, disallowed in 51. historically, it was used to implement finally. - op == Opcodes.GOTO || isConditionalJump(instruction) + op == GOTO || isConditionalJump(instruction) } def isConditionalJump(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - (op >= Opcodes.IFEQ && op <= Opcodes.IF_ACMPNE) || op == Opcodes.IFNULL || op == Opcodes.IFNONNULL + (op >= IFEQ && op <= IF_ACMPNE) || op == IFNULL || op == IFNONNULL } def isReturn(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - op >= Opcodes.IRETURN && op <= Opcodes.RETURN + op >= IRETURN && op <= RETURN } def isLoad(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - op >= Opcodes.ILOAD && op <= Opcodes.ALOAD + op >= ILOAD && op <= ALOAD } def isStore(instruction: AbstractInsnNode): Boolean = { val op = instruction.getOpcode - op >= Opcodes.ISTORE && op <= Opcodes.ASTORE + op >= ISTORE && op <= ASTORE } def isVarInstruction(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction) @@ -88,21 +89,21 @@ object BytecodeUtils { methodNode.name == INSTANCE_CONSTRUCTOR_NAME || methodNode.name == CLASS_CONSTRUCTOR_NAME } - def isStaticMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_STATIC) != 0 + def isStaticMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_STATIC) != 0 - def isAbstractMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_ABSTRACT) != 0 + def isAbstractMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_ABSTRACT) != 0 - def isSynchronizedMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_SYNCHRONIZED) != 0 + def isSynchronizedMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_SYNCHRONIZED) != 0 - def isNativeMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_NATIVE) != 0 + def isNativeMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_NATIVE) != 0 def hasCallerSensitiveAnnotation(methodNode: MethodNode) = methodNode.visibleAnnotations != null && methodNode.visibleAnnotations.asScala.exists(_.desc == "Lsun/reflect/CallerSensitive;") - def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & Opcodes.ACC_FINAL) != 0 + def isFinalClass(classNode: ClassNode): Boolean = (classNode.access & ACC_FINAL) != 0 - def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (Opcodes.ACC_FINAL | Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC)) != 0 + def isFinalMethod(methodNode: MethodNode): Boolean = (methodNode.access & (ACC_FINAL | ACC_PRIVATE | ACC_STATIC)) != 0 - def isStrictfpMethod(methodNode: MethodNode): Boolean = (methodNode.access & Opcodes.ACC_STRICT) != 0 + def isStrictfpMethod(methodNode: MethodNode): Boolean = (methodNode.access & ACC_STRICT) != 0 def isReference(t: Type) = t.getSort == Type.OBJECT || t.getSort == Type.ARRAY @@ -124,14 +125,14 @@ object BytecodeUtils { def removeJumpAndAdjustStack(method: MethodNode, jump: JumpInsnNode) { val instructions = method.instructions val op = jump.getOpcode - if ((op >= Opcodes.IFEQ && op <= Opcodes.IFGE) || op == Opcodes.IFNULL || op == Opcodes.IFNONNULL) { + if ((op >= IFEQ && op <= IFGE) || op == IFNULL || op == IFNONNULL) { instructions.insert(jump, getPop(1)) - } else if ((op >= Opcodes.IF_ICMPEQ && op <= Opcodes.IF_ICMPLE) || op == Opcodes.IF_ACMPEQ || op == Opcodes.IF_ACMPNE) { + } else if ((op >= IF_ICMPEQ && op <= IF_ICMPLE) || op == IF_ACMPEQ || op == IF_ACMPNE) { instructions.insert(jump, getPop(1)) instructions.insert(jump, getPop(1)) } else { // we can't remove JSR: its execution does not only jump, it also adds a return address to the stack - assert(jump.getOpcode == Opcodes.GOTO) + assert(jump.getOpcode == GOTO) } instructions.remove(jump) } @@ -148,43 +149,50 @@ object BytecodeUtils { } def negateJumpOpcode(jumpOpcode: Int): Int = (jumpOpcode: @switch) match { - case Opcodes.IFEQ => Opcodes.IFNE - case Opcodes.IFNE => Opcodes.IFEQ + case IFEQ => IFNE + case IFNE => IFEQ - case Opcodes.IFLT => Opcodes.IFGE - case Opcodes.IFGE => Opcodes.IFLT + case IFLT => IFGE + case IFGE => IFLT - case Opcodes.IFGT => Opcodes.IFLE - case Opcodes.IFLE => Opcodes.IFGT + case IFGT => IFLE + case IFLE => IFGT - case Opcodes.IF_ICMPEQ => Opcodes.IF_ICMPNE - case Opcodes.IF_ICMPNE => Opcodes.IF_ICMPEQ + case IF_ICMPEQ => IF_ICMPNE + case IF_ICMPNE => IF_ICMPEQ - case Opcodes.IF_ICMPLT => Opcodes.IF_ICMPGE - case Opcodes.IF_ICMPGE => Opcodes.IF_ICMPLT + case IF_ICMPLT => IF_ICMPGE + case IF_ICMPGE => IF_ICMPLT - case Opcodes.IF_ICMPGT => Opcodes.IF_ICMPLE - case Opcodes.IF_ICMPLE => Opcodes.IF_ICMPGT + case IF_ICMPGT => IF_ICMPLE + case IF_ICMPLE => IF_ICMPGT - case Opcodes.IF_ACMPEQ => Opcodes.IF_ACMPNE - case Opcodes.IF_ACMPNE => Opcodes.IF_ACMPEQ + case IF_ACMPEQ => IF_ACMPNE + case IF_ACMPNE => IF_ACMPEQ - case Opcodes.IFNULL => Opcodes.IFNONNULL - case Opcodes.IFNONNULL => Opcodes.IFNULL + case IFNULL => IFNONNULL + case IFNONNULL => IFNULL } def isSize2LoadOrStore(opcode: Int): Boolean = (opcode: @switch) match { - case Opcodes.LLOAD | Opcodes.DLOAD | Opcodes.LSTORE | Opcodes.DSTORE => true + case LLOAD | DLOAD | LSTORE | DSTORE => true case _ => false } def getPop(size: Int): InsnNode = { - val op = if (size == 1) Opcodes.POP else Opcodes.POP2 + val op = if (size == 1) POP else POP2 new InsnNode(op) } def instructionResultSize(instruction: AbstractInsnNode) = InstructionResultSize(instruction) + /** + * The number of local variable slots used for parameters and for the `this` reference. + */ + def parametersSize(methodNode: MethodNode): Int = { + (Type.getArgumentsAndReturnSizes(methodNode.desc) >> 2) - (if (isStaticMethod(methodNode)) 1 else 0) + } + def labelReferences(method: MethodNode): Map[LabelNode, Set[AnyRef]] = { val res = mutable.Map.empty[LabelNode, Set[AnyRef]] def add(l: LabelNode, ref: AnyRef) = if (res contains l) res(l) = res(l) + ref else res(l) = Set(ref) @@ -311,10 +319,10 @@ object BytecodeUtils { */ def fixLoadedNothingOrNullValue(loadedType: Type, loadInstr: AbstractInsnNode, methodNode: MethodNode, bTypes: BTypes): Unit = { if (loadedType == bTypes.coreBTypes.srNothingRef.toASMType) { - methodNode.instructions.insert(loadInstr, new InsnNode(Opcodes.ATHROW)) + methodNode.instructions.insert(loadInstr, new InsnNode(ATHROW)) } else if (loadedType == bTypes.coreBTypes.srNullRef.toASMType) { - methodNode.instructions.insert(loadInstr, new InsnNode(Opcodes.ACONST_NULL)) - methodNode.instructions.insert(loadInstr, new InsnNode(Opcodes.POP)) + methodNode.instructions.insert(loadInstr, new InsnNode(ACONST_NULL)) + methodNode.instructions.insert(loadInstr, new InsnNode(POP)) } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index a80b3d0487..25e4bb1593 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -9,6 +9,7 @@ package opt import scala.annotation.switch import scala.tools.asm.{Type, ClassWriter, MethodWriter, Opcodes} +import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.BTypes.InternalName @@ -193,7 +194,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { var i = 0 var liveLabels = Set.empty[LabelNode] var removedInstructions = Set.empty[AbstractInsnNode] - var maxLocals = (Type.getArgumentsAndReturnSizes(method.desc) >> 2) - (if (BytecodeUtils.isStaticMethod(method)) 1 else 0) + var maxLocals = parametersSize(method) var maxStack = 0 val itr = method.instructions.iterator() while (itr.hasNext) { @@ -214,7 +215,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { maxLocals = math.max(maxLocals, i.`var` + 1) case _ => - if (!isLive || insn.getOpcode == Opcodes.NOP) { + if (!isLive || insn.getOpcode == NOP) { // Instruction iterators allow removing during iteration. // Removing is O(1): instructions are doubly linked list elements. itr.remove() @@ -247,7 +248,7 @@ object LocalOptImpls { def containsExecutableCode(start: AbstractInsnNode, end: LabelNode): Boolean = { start != end && ((start.getOpcode : @switch) match { // FrameNode, LabelNode and LineNumberNode have opcode == -1. - case -1 | Opcodes.GOTO => containsExecutableCode(start.getNext, end) + case -1 | GOTO => containsExecutableCode(start.getNext, end) case _ => true }) } @@ -294,17 +295,6 @@ object LocalOptImpls { method.localVariables.size != initialNumVars } - /** - * The number of local variable slots used for parameters and for the `this` reference. - */ - private def parametersSize(method: MethodNode): Int = { - // Double / long fields occupy two slots, so we sum up the sizes. Since getSize returns 0 for - // void, we have to add `max 1`. - val paramsSize = scala.tools.asm.Type.getArgumentTypes(method.desc).iterator.map(_.getSize max 1).sum - val thisSize = if ((method.access & Opcodes.ACC_STATIC) == 0) 1 else 0 - paramsSize + thisSize - } - /** * Compact the local variable slots used in the method's implementation. This prevents having * unused slots for example after eliminating unreachable code. @@ -579,7 +569,7 @@ object LocalOptImpls { case Goto(jump) => nextExecutableInstruction(jump.label) match { case Some(target) => - if (isReturn(target) || target.getOpcode == Opcodes.ATHROW) { + if (isReturn(target) || target.getOpcode == ATHROW) { method.instructions.set(jump, target.clone(null)) true } else false -- cgit v1.2.3 From 09eba3f480a32c9af366ede4b888735a18e129ad Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Thu, 29 Oct 2015 20:14:53 +0100 Subject: Fix VarInstruction extractor to include IINC Previously the VarInstruction extractor did not include IINCs. Also give rename the isVarInstruction predicate to isLoadStoreOrRet, as it doesn't include IINC. Also fixes a bug in removeJumpAndAdjustStack, it checked the wrong opcode range before. --- .../nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala | 2 +- .../scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 11 +++++++---- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 16 +++++++++------- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala index c933341492..f7ec55f305 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala @@ -157,7 +157,7 @@ trait ProdConsAnalyzerImpl { } private def isCopyOperation(insn: AbstractInsnNode): Boolean = { - isVarInstruction(insn) || { + isLoadOrStore(insn) || { (insn.getOpcode: @switch) match { case DUP | DUP_X1 | DUP_X2 | DUP2 | DUP2_X1 | DUP2_X2 | SWAP | CHECKCAST => true case _ => false diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index c6f88a4c98..693f735e65 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -48,8 +48,9 @@ object BytecodeUtils { } object VarInstruction { - def unapply(instruction: AbstractInsnNode): Option[VarInsnNode] = { - if (isVarInstruction(instruction)) Some(instruction.asInstanceOf[VarInsnNode]) + def unapply(instruction: AbstractInsnNode): Option[(AbstractInsnNode, Int)] = { + if (isLoadStoreOrRet(instruction)) Some((instruction, instruction.asInstanceOf[VarInsnNode].`var`)) + else if (instruction.getOpcode == IINC) Some((instruction, instruction.asInstanceOf[IincInsnNode].`var`)) else None } @@ -81,7 +82,9 @@ object BytecodeUtils { op >= ISTORE && op <= ASTORE } - def isVarInstruction(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction) + def isLoadStoreOrRet(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction) || instruction.getOpcode == RET + + def isLoadOrStore(instruction: AbstractInsnNode): Boolean = isLoad(instruction) || isStore(instruction) def isExecutable(instruction: AbstractInsnNode): Boolean = instruction.getOpcode >= 0 @@ -125,7 +128,7 @@ object BytecodeUtils { def removeJumpAndAdjustStack(method: MethodNode, jump: JumpInsnNode) { val instructions = method.instructions val op = jump.getOpcode - if ((op >= IFEQ && op <= IFGE) || op == IFNULL || op == IFNONNULL) { + if ((op >= IFEQ && op <= IFLE) || op == IFNULL || op == IFNONNULL) { instructions.insert(jump, getPop(1)) } else if ((op >= IF_ICMPEQ && op <= IF_ICMPLE) || op == IF_ACMPEQ || op == IF_ACMPNE) { instructions.insert(jump, getPop(1)) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 25e4bb1593..afbf9cf2ce 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -310,8 +310,8 @@ object LocalOptImpls { val renumber = collection.mutable.ArrayBuffer.empty[Int] // Add the index of the local variable used by `varIns` to the `renumber` array. - def addVar(varIns: VarInsnNode): Unit = { - val index = varIns.`var` + def addVar(varIns: AbstractInsnNode, slot: Int): Unit = { + val index = slot val isWide = isSize2LoadOrStore(varIns.getOpcode) // Ensure the length of `renumber`. Unused variable indices are mapped to -1. @@ -329,7 +329,7 @@ object LocalOptImpls { val firstLocalIndex = parametersSize(method) for (i <- 0 until firstLocalIndex) renumber += i // parameters and `this` are always used. method.instructions.iterator().asScala foreach { - case VarInstruction(varIns) => addVar(varIns) + case VarInstruction(varIns, slot) => addVar(varIns, slot) case _ => } @@ -350,10 +350,12 @@ object LocalOptImpls { // update variable instructions according to the renumber table method.maxLocals = nextIndex method.instructions.iterator().asScala.foreach { - case VarInstruction(varIns) => - val oldIndex = varIns.`var` - if (oldIndex >= firstLocalIndex && renumber(oldIndex) != oldIndex) - varIns.`var` = renumber(varIns.`var`) + case VarInstruction(varIns, slot) => + val oldIndex = slot + if (oldIndex >= firstLocalIndex && renumber(oldIndex) != oldIndex) varIns match { + case vi: VarInsnNode => vi.`var` = renumber(slot) + case ii: IincInsnNode => ii.`var` = renumber(slot) + } case _ => } true -- cgit v1.2.3 From 7f2a0a2046e9ea16e5eaab03841ad2a7fccba8cf Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Thu, 29 Oct 2015 20:34:29 +0100 Subject: Remove InstructionResultSize (subsumed by InstructionStackEffect) --- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 3 +- .../backend/jvm/opt/InstructionResultSize.scala | 236 --------------------- 2 files changed, 2 insertions(+), 237 deletions(-) delete mode 100644 src/compiler/scala/tools/nsc/backend/jvm/opt/InstructionResultSize.scala diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index 693f735e65..862621c7c4 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -17,6 +17,7 @@ import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import GenBCode._ import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.analysis.InstructionStackEffect object BytecodeUtils { @@ -187,7 +188,7 @@ object BytecodeUtils { new InsnNode(op) } - def instructionResultSize(instruction: AbstractInsnNode) = InstructionResultSize(instruction) + def instructionResultSize(insn: AbstractInsnNode) = InstructionStackEffect.prod(InstructionStackEffect.forClassfile(insn)) /** * The number of local variable slots used for parameters and for the `this` reference. diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/InstructionResultSize.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/InstructionResultSize.scala deleted file mode 100644 index 79e44a8503..0000000000 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/InstructionResultSize.scala +++ /dev/null @@ -1,236 +0,0 @@ -package scala.tools.nsc.backend.jvm.opt - -import scala.annotation.switch -import scala.tools.asm.{Handle, Type, Opcodes} -import scala.tools.asm.tree._ - -object InstructionResultSize { - import Opcodes._ - def apply(instruction: AbstractInsnNode): Int = (instruction.getOpcode: @switch) match { - // The order of opcodes is (almost) the same as in Opcodes.java - case ACONST_NULL => 1 - - case ICONST_M1 | - ICONST_0 | - ICONST_1 | - ICONST_2 | - ICONST_3 | - ICONST_4 | - ICONST_5 => 1 - - case LCONST_0 | - LCONST_1 => 2 - - case FCONST_0 | - FCONST_1 | - FCONST_2 => 1 - - case DCONST_0 | - DCONST_1 => 2 - - case BIPUSH | - SIPUSH => 1 - - case LDC => - instruction.asInstanceOf[LdcInsnNode].cst match { - case _: java.lang.Long | - _: java.lang.Double => 2 - - case _ => 1 - } - - case ILOAD | - FLOAD | - ALOAD => 1 - - case LLOAD | - DLOAD => 2 - - case IALOAD | - FALOAD | - AALOAD | - BALOAD | - CALOAD | - SALOAD => 1 - - case LALOAD | - DALOAD => 2 - - case ISTORE | - LSTORE | - FSTORE | - DSTORE | - ASTORE => 0 - - case IASTORE | - LASTORE | - FASTORE | - DASTORE | - AASTORE | - BASTORE | - CASTORE | - SASTORE => 0 - - case POP | - POP2 => 0 - - case DUP | - DUP_X1 | - DUP_X2 | - DUP2 | - DUP2_X1 | - DUP2_X2 | - SWAP => throw new IllegalArgumentException("Can't compute the size of DUP/SWAP without knowing what's on stack top") - - case IADD | - FADD => 1 - - case LADD | - DADD => 2 - - case ISUB | - FSUB => 1 - - case LSUB | - DSUB => 2 - - case IMUL | - FMUL => 1 - - case LMUL | - DMUL => 2 - - case IDIV | - FDIV => 1 - - case LDIV | - DDIV => 2 - - case IREM | - FREM => 1 - - case LREM | - DREM => 2 - - case INEG | - FNEG => 1 - - case LNEG | - DNEG => 2 - - case ISHL | - ISHR => 1 - - case LSHL | - LSHR => 2 - - case IUSHR => 1 - - case LUSHR => 2 - - case IAND | - IOR | - IXOR => 1 - - case LAND | - LOR | - LXOR => 2 - - case IINC => 1 - - case I2F | - L2I | - L2F | - F2I | - D2I | - D2F | - I2B | - I2C | - I2S => 1 - - case I2L | - I2D | - L2D | - F2L | - F2D | - D2L => 2 - - case LCMP | - FCMPL | - FCMPG | - DCMPL | - DCMPG => 1 - - case IFEQ | - IFNE | - IFLT | - IFGE | - IFGT | - IFLE => 0 - - case IF_ICMPEQ | - IF_ICMPNE | - IF_ICMPLT | - IF_ICMPGE | - IF_ICMPGT | - IF_ICMPLE | - IF_ACMPEQ | - IF_ACMPNE => 0 - - case GOTO => 0 - - case JSR => throw new IllegalArgumentException("Subroutines are not supported.") - - case RET => 0 - - case TABLESWITCH | - LOOKUPSWITCH => 0 - - case IRETURN | - FRETURN | - ARETURN => 1 - - case LRETURN | - DRETURN => 2 - - case RETURN => 0 - - case GETSTATIC => Type.getType(instruction.asInstanceOf[FieldInsnNode].desc).getSize - - case PUTSTATIC => 0 - - case GETFIELD => Type.getType(instruction.asInstanceOf[FieldInsnNode].desc).getSize - - case PUTFIELD => 0 - - case INVOKEVIRTUAL | - INVOKESPECIAL | - INVOKESTATIC | - INVOKEINTERFACE => - val desc = instruction.asInstanceOf[MethodInsnNode].desc - Type.getReturnType(desc).getSize - - case INVOKEDYNAMIC => - val desc = instruction.asInstanceOf[InvokeDynamicInsnNode].desc - Type.getReturnType(desc).getSize - - case NEW => 1 - - case NEWARRAY | - ANEWARRAY | - ARRAYLENGTH => 1 - - case ATHROW => 0 - - case CHECKCAST | - INSTANCEOF => 1 - - case MONITORENTER | - MONITOREXIT => 0 - - case MULTIANEWARRAY => 1 - - case IFNULL | - IFNONNULL => 0 - } -} -- cgit v1.2.3 From 5d4d2cc9f95d88dc4ba19095c6cfe8b60ad1a21d Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Thu, 29 Oct 2015 21:35:06 +0100 Subject: Clean up DCE: remove eliminated callsites from call graph earlier When running DCE, directly remove eliminated callsites from the call graph (instead delegating this task to the callees). --- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 11 +---- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 55 ++++++++++++---------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index 449a56fdd1..d1bccf614e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -26,14 +26,6 @@ class Inliner[BT <: BTypes](val btypes: BT) { import inlinerHeuristics._ import backendUtils._ - def eliminateUnreachableCodeAndUpdateCallGraph(methodNode: MethodNode, definingClass: InternalName): Unit = { - localOpt.minimalRemoveUnreachableCode(methodNode, definingClass) foreach { - case invocation: MethodInsnNode => callGraph.removeCallsite(invocation, methodNode) - case indy: InvokeDynamicInsnNode => callGraph.removeClosureInstantiation(indy, methodNode) - case _ => - } - } - def runInliner(): Unit = { rewriteFinalTraitMethodInvocations() @@ -136,7 +128,6 @@ class Inliner[BT <: BTypes](val btypes: BT) { if (!selfTypeOk) { // We don't need to worry about the method being too large for running an analysis. // Callsites of large methods are not added to the call graph. - localOpt.minimalRemoveUnreachableCode(callsite.callsiteMethod, callsite.callsiteClass.internalName) val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new Analyzer(new SourceInterpreter)) val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekStack(traitMethodArgumentTypes.length) for (i <- receiverValue.insns.asScala) { @@ -317,7 +308,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { // If we have an inline request for a call to g, and f has been already inlined into g, we // need to run DCE before inlining g. val Right(callee) = request.callsite.callee - eliminateUnreachableCodeAndUpdateCallGraph(callee.callee, callee.calleeDeclarationClass.internalName) + localOpt.minimalRemoveUnreachableCode(callee.callee, callee.calleeDeclarationClass.internalName) // Skip over DCE'd callsites if (callGraph.containsCallsite(request.callsite)) { inlineCallsite(request.callsite) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index afbf9cf2ce..f1392f7b7c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -60,28 +60,27 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { * * @return A set containing the eliminated instructions */ - def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Set[AbstractInsnNode] = { - if (method.instructions.size == 0) return Set.empty // fast path for abstract methods - if (unreachableCodeEliminated(method)) return Set.empty // we know there is no unreachable code - if (!AsmAnalyzer.sizeOKForBasicValue(method)) return Set.empty // the method is too large for running an analyzer + def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = { + if (method.instructions.size == 0) return false // fast path for abstract methods + if (unreachableCodeEliminated(method)) return false // we know there is no unreachable code + if (!AsmAnalyzer.sizeOKForBasicValue(method)) return false // the method is too large for running an analyzer // For correctness, after removing unreachable code, we have to eliminate empty exception // handlers, see scaladoc of def methodOptimizations. Removing an live handler may render more // code unreachable and therefore requires running another round. - def removalRound(): Set[AbstractInsnNode] = { - val (removedInstructions, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) - val removedRecursively = if (removedInstructions.nonEmpty) { + def removalRound(): Boolean = { + val (insnsRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) + if (insnsRemoved) { val liveHandlerRemoved = removeEmptyExceptionHandlers(method).exists(h => liveLabels(h.start)) if (liveHandlerRemoved) removalRound() - else Set.empty - } else Set.empty - removedInstructions ++ removedRecursively + } + insnsRemoved } - val removedInstructions = removalRound() - if (removedInstructions.nonEmpty) removeUnusedLocalVariableNodes(method)() + val changed = removalRound() + if (changed) removeUnusedLocalVariableNodes(method)() unreachableCodeEliminated += method - removedInstructions + changed } /** @@ -139,15 +138,12 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // This triggers "ClassFormatError: Illegal exception table range in class file C". Similar // for local variables in dead blocks. Maybe that's a bug in the ASM framework. - def canRunDCE = AsmAnalyzer.sizeOKForBasicValue(method) def removalRound(): Boolean = { // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) - val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (compilerSettings.YoptUnreachableCode && canRunDCE) { - val (removedInstructions, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) + val (codeRemoved, handlersRemoved, liveHandlerRemoved) = { + val (insnsRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) val removedHandlers = removeEmptyExceptionHandlers(method) - (removedInstructions.nonEmpty, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) - } else { - (false, false, false) + (insnsRemoved, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) } val jumpsChanged = if (compilerSettings.YoptSimplifyJumps) simplifyJumps(method) else false @@ -159,7 +155,13 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { codeRemoved || handlersRemoved || jumpsChanged } - val codeHandlersOrJumpsChanged = removalRound() + val codeHandlersOrJumpsChanged = if (compilerSettings.YoptUnreachableCode && AsmAnalyzer.sizeOKForBasicValue(method)) { + // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE + // here is more thorough than `minimalRemoveUnreachableCode` + val r = removalRound() + unreachableCodeEliminated += method + r + } else false // (*) Removing stale local variable descriptors is required for correctness of unreachable-code val localsRemoved = @@ -187,13 +189,13 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { * * @return A set containing eliminated instructions, and a set containing all live label nodes. */ - def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Set[AbstractInsnNode], Set[LabelNode]) = { + def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Boolean, Set[LabelNode]) = { val a = new AsmAnalyzer(method, ownerClassName) val frames = a.analyzer.getFrames var i = 0 var liveLabels = Set.empty[LabelNode] - var removedInstructions = Set.empty[AbstractInsnNode] + var changed = false var maxLocals = parametersSize(method) var maxStack = 0 val itr = method.instructions.iterator() @@ -219,14 +221,19 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // Instruction iterators allow removing during iteration. // Removing is O(1): instructions are doubly linked list elements. itr.remove() - removedInstructions += insn + changed = true + insn match { + case invocation: MethodInsnNode => callGraph.removeCallsite(invocation, method) + case indy: InvokeDynamicInsnNode => callGraph.removeClosureInstantiation(indy, method) + case _ => + } } } i += 1 } method.maxLocals = maxLocals method.maxStack = maxStack - (removedInstructions, liveLabels) + (changed, liveLabels) } } -- cgit v1.2.3 From f777790250ffe92b65b51c71c7da113659177ad0 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 6 Nov 2015 09:31:59 +0100 Subject: Fix merging of AliasingFrames By convention (for performance), `aliasingFrame.aliases(slot) == null` means that the value at `slot` has no aliases, so this is isomorphic to having a singleton `AliasSet(slot)`. When merging an AliasingFrame `other` into `this`, at every value slot we have to keep the intersection of the two alias sets. In other words we have to remove from `this.aliases(slot)` those values that are not in `other.aliases(slot)`. This was wrong for the cases where `other.aliases == null` - we did not treat this case as a singleton set. --- .../nsc/backend/jvm/analysis/AliasingFrame.scala | 35 ++++++++++++++-------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala index 5aa060e019..086946e4e3 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/AliasingFrame.scala @@ -28,6 +28,8 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc init(src) } + override def toString: String = super.toString + " - " + aliases.toList.filter(s => s != null && s.size > 1).map(_.toString).distinct.mkString(",") + /** * For every value the set of values that are aliases of it. * @@ -299,18 +301,25 @@ class AliasingFrame[V <: Value](nLocals: Int, nStack: Int) extends Frame[V](nLoc if (!knownOk(i)) { val thisAliases = this.aliases(i) val otherAliases = aliasingOther.aliases(i) - if (thisAliases != null && otherAliases != null) { - // The iterator yields elements that are in `thisAliases` but not in `otherAliases`. - // As a side-effect, for every index `i` that is in both alias sets, the iterator sets - // `knownOk(i) = true`: the alias sets for these values don't need to be merged again. - val thisNotOtherIt = AliasSet.andNotIterator(thisAliases, otherAliases, knownOk) - if (thisNotOtherIt.hasNext) { - aliasesChanged = true - val newSet = AliasSet.empty - while (thisNotOtherIt.hasNext) { - val next = thisNotOtherIt.next() - newSet += next - setAliasSet(next, newSet) + if (thisAliases != null) { + if (otherAliases == null) { + if (thisAliases.size > 1) { + aliasesChanged = true + removeAlias(i) + } + } else { + // The iterator yields elements that are in `thisAliases` but not in `otherAliases`. + // As a side-effect, for every index `i` that is in both alias sets, the iterator sets + // `knownOk(i) = true`: the alias sets for these values don't need to be merged again. + val thisNotOtherIt = AliasSet.andNotIterator(thisAliases, otherAliases, knownOk) + if (thisNotOtherIt.hasNext) { + aliasesChanged = true + val newSet = AliasSet.empty + while (thisNotOtherIt.hasNext) { + val next = thisNotOtherIt.next() + newSet += next + setAliasSet(next, newSet) + } } } } @@ -416,7 +425,7 @@ abstract class IntIterator extends Iterator[Int] { class AliasSet(var set: Object /*SmallBitSet | Array[Long]*/, var size: Int) { import AliasSet._ - override def toString: String = set.toString + override def toString: String = iterator.toSet.mkString("<", ",", ">") /** * An iterator for the elements of this bit set. Note that only one iterator can be used at a -- cgit v1.2.3 From 29d772d0655a3a5af79c3d8eca91bb33a82c7194 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 6 Nov 2015 09:38:21 +0100 Subject: Copy propagation, remove unused values (closures!) and local variables Copy propagation uses an AliasingAnalyzer: it replaces a `LOAD n` instruction by `LOAD m` where m is the smallest alias of n. This leads to stale STORE instructions. Stale STOREs are identified using a ProdCons analyzer and replaced by POPs. Values that are pushed on the stack by a side-effect free instruction and consumed by a POP are then removed by `eliminatePushPop`. This includes elimination of unused closure allocations and unused boxes and tuple allocations (*). A final cleanup eliminates `STORE x; LOADx` pairs where the stored value is not otherwise used. Fixes - https://github.com/scala/scala-dev/issues/25 - https://github.com/scala/scala-dev/issues/7 - https://github.com/scala/scala-dev/issues/14 - https://github.com/scala/scala-dev/issues/12 (*) We don't yet rewrite reads of boxes and tuples yet. For example, `val x = (1, 2); x._1` remains a method invocation and the tuple cannot be eliminated (https://github.com/scala/scala-dev/issues/11). Inspired in many ways by Miguel's work! --- .../nsc/backend/jvm/analysis/BackendUtils.scala | 1 + .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 159 +++- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 814 +++++++++++++++++++-- .../scala/tools/nsc/settings/ScalaSettings.scala | 4 +- .../nsc/backend/jvm/opt/ClosureOptimizerTest.scala | 4 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 43 +- .../nsc/backend/jvm/opt/MethodLevelOpts.scala | 164 ++++- 7 files changed, 1104 insertions(+), 85 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala index b02bc7c96e..204c6a33ce 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala @@ -47,6 +47,7 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { private val basicValueSizeLimit = 9000l * 1000l * 1000l private val sourceValueSizeLimit = 8000l * 950l * 950l + def sizeOKForAliasing(method: MethodNode): Boolean = size(method) < nullnessSizeLimit def sizeOKForNullness(method: MethodNode): Boolean = size(method) < nullnessSizeLimit def sizeOKForBasicValue(method: MethodNode): Boolean = size(method) < basicValueSizeLimit def sizeOKForSourceValue(method: MethodNode): Boolean = size(method) < sourceValueSizeLimit diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index 862621c7c4..3854bc840d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -17,6 +17,7 @@ import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import GenBCode._ import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.tools.nsc.backend.jvm.analysis.InstructionStackEffect object BytecodeUtils { @@ -111,11 +112,16 @@ object BytecodeUtils { def isReference(t: Type) = t.getSort == Type.OBJECT || t.getSort == Type.ARRAY - def nextExecutableInstruction(instruction: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { - var result = instruction - do { result = result.getNext } - while (result != null && !isExecutable(result) && !alsoKeep(result)) - Option(result) + @tailrec def nextExecutableInstruction(insn: AbstractInsnNode, alsoKeep: AbstractInsnNode => Boolean = Set()): Option[AbstractInsnNode] = { + val next = insn.getNext + if (next == null || isExecutable(next) || alsoKeep(next)) Option(next) + else nextExecutableInstruction(next, alsoKeep) + } + + @tailrec def nextExecutableInstructionOrLabel(insn: AbstractInsnNode): Option[AbstractInsnNode] = { + val next = insn.getNext + if (next == null || isExecutable(next) || next.isInstanceOf[LabelNode]) Option(next) + else nextExecutableInstructionOrLabel(next) } def sameTargetExecutableInstruction(a: JumpInsnNode, b: JumpInsnNode): Boolean = { @@ -330,6 +336,149 @@ object BytecodeUtils { } } + def isSideEffectFreeCall(insn: MethodInsnNode): Boolean = { + isScalaBox(insn) || isScalaUnbox(insn) || + isJavaBox(insn) || // not java unbox, it may NPE + isSideEffectFreeConstructorCall(insn) + } + + private val srBoxesRuntimeName = "scala/runtime/BoxesRunTime" + + def isScalaBox(insn: MethodInsnNode): Boolean = { + insn.owner == srBoxesRuntimeName && { + val args = Type.getArgumentTypes(insn.desc) + args.length == 1 && ((args(0).getSort: @switch) match { + case Type.BOOLEAN => insn.name == "boxToBoolean" && insn.desc == "(Z)Ljava/lang/Boolean;" + case Type.BYTE => insn.name == "boxToByte" && insn.desc == "(B)Ljava/lang/Byte;" + case Type.CHAR => insn.name == "boxToCharacter" && insn.desc == "(C)Ljava/lang/Character;" + case Type.SHORT => insn.name == "boxToShort" && insn.desc == "(S)Ljava/lang/Short;" + case Type.INT => insn.name == "boxToInteger" && insn.desc == "(I)Ljava/lang/Integer;" + case Type.LONG => insn.name == "boxToLong" && insn.desc == "(J)Ljava/lang/Long;" + case Type.FLOAT => insn.name == "boxToFloat" && insn.desc == "(F)Ljava/lang/Float;" + case Type.DOUBLE => insn.name == "boxToDouble" && insn.desc == "(D)Ljava/lang/Double;" + case _ => false + }) + } + } + + def isScalaUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == srBoxesRuntimeName && ((Type.getReturnType(insn.desc).getSort: @switch) match { + case Type.BOOLEAN => insn.name == "unboxToBoolean" && insn.desc == "(Ljava/lang/Object;)Z" + case Type.BYTE => insn.name == "unboxToByte" && insn.desc == "(Ljava/lang/Object;)B" + case Type.CHAR => insn.name == "unboxToChar" && insn.desc == "(Ljava/lang/Object;)C" + case Type.SHORT => insn.name == "unboxToShort" && insn.desc == "(Ljava/lang/Object;)S" + case Type.INT => insn.name == "unboxToInt" && insn.desc == "(Ljava/lang/Object;)I" + case Type.LONG => insn.name == "unboxToLong" && insn.desc == "(Ljava/lang/Object;)J" + case Type.FLOAT => insn.name == "unboxToFloat" && insn.desc == "(Ljava/lang/Object;)F" + case Type.DOUBLE => insn.name == "unboxToDouble" && insn.desc == "(Ljava/lang/Object;)D" + case _ => false + }) + } + + def isJavaBox(insn: MethodInsnNode): Boolean = { + insn.name == "valueOf" && { + val args = Type.getArgumentTypes(insn.desc) + args.length == 1 && ((args(0).getSort: @switch) match { + case Type.BOOLEAN => insn.owner == "java/lang/Boolean" && insn.desc == "(Z)Ljava/lang/Boolean;" + case Type.BYTE => insn.owner == "java/lang/Byte" && insn.desc == "(B)Ljava/lang/Byte;" + case Type.CHAR => insn.owner == "java/lang/Character" && insn.desc == "(C)Ljava/lang/Character;" + case Type.SHORT => insn.owner == "java/lang/Short" && insn.desc == "(S)Ljava/lang/Short;" + case Type.INT => insn.owner == "java/lang/Integer" && insn.desc == "(I)Ljava/lang/Integer;" + case Type.LONG => insn.owner == "java/lang/Long" && insn.desc == "(J)Ljava/lang/Long;" + case Type.FLOAT => insn.owner == "java/lang/Float" && insn.desc == "(F)Ljava/lang/Float;" + case Type.DOUBLE => insn.owner == "java/lang/Double" && insn.desc == "(D)Ljava/lang/Double;" + case _ => false + }) + } + } + + // unused objects created by these constructors are eliminated by pushPop + private val sideEffectFreeConstructors = Set( + "java/lang/Object()V", + "java/lang/String()V", + "java/lang/String(Ljava/lang/String;)V", + "java/lang/String([C)V", + + "java/lang/Boolean(Z)V", + "java/lang/Byte(B)V", + "java/lang/Character(C)V", + "java/lang/Short(S)V", + "java/lang/Integer(I)V", + "java/lang/Long(J)V", + "java/lang/Float(F)V", + "java/lang/Double(D)V", + + "scala/runtime/ObjectRef(Ljava/lang/Object;)V", + "scala/runtime/BooleanRef(Z)V", + "scala/runtime/ByteRef(B)V", + "scala/runtime/CharRef(C)V", + "scala/runtime/ShortRef(S)V", + "scala/runtime/IntRef(I)V", + "scala/runtime/LongRef(J)V", + "scala/runtime/FloatRef(F)V", + "scala/runtime/DoubleRef(D)V", + + "scala/runtime/VolatileObjectRef(Ljava/lang/Object;)V", + "scala/runtime/VolatileBooleanRef(Z)V", + "scala/runtime/VolatileByteRef(B)V", + "scala/runtime/VolatileCharRef(C)V", + "scala/runtime/VolatileShortRef(S)V", + "scala/runtime/VolatileIntRef(I)V", + "scala/runtime/VolatileLongRef(J)V", + "scala/runtime/VolatileFloatRef(F)V", + "scala/runtime/VolatileDoubleRef(D)V" + ) ++ { + (1 to 22).map(n => "scala/Tuple" + n + "(" + ("Ljava/lang/Object;" * n) + ")V") + } ++ { + Iterator("I", "J", "D").map(t => "scala/Tuple1$mc" + t + "$sp(" + t + ")V") + } ++ { + def tuple2Specs = Iterator("I", "J", "D", "C", "Z") + for (a <- tuple2Specs; b <- tuple2Specs) yield "scala/Tuple2$mc" + a + b + "$sp(" + a + b + ")V" + } + + def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { + insn.name == INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors(insn.owner + insn.desc) + } + + private val classesForSideEffectFreeConstructors = sideEffectFreeConstructors.map(s => s.substring(0, s.indexOf('('))) + + // we only eliminate `NEW C` if the class C has a constructor that we consider side-effect free. + // removing a `NEW` eliminates a potential NoClassDefFoundError, so we only do it for core classes. + def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode) = { + insn.getOpcode == NEW && { + val ti = insn.asInstanceOf[TypeInsnNode] + classesForSideEffectFreeConstructors(ti.desc) + } + } + + def isBoxedUnit(insn: AbstractInsnNode) = { + insn.getOpcode == GETSTATIC && { + val fi = insn.asInstanceOf[FieldInsnNode] + fi.owner == "scala/runtime/BoxedUnit" && fi.name == "UNIT" && fi.desc == "Lscala/runtime/BoxedUnit;" + } + } + + private def buildFunctionTypes(base: String): Set[InternalName] = { + def primitives = Iterator("B", "S", "I", "J", "C", "F", "D", "Z", "V") + def ijfd = Iterator("I", "J", "F", "D") + def ijd = Iterator("I", "J", "D") + Set.empty[String] ++ { + (0 to 22).map(base + _) + } ++ { + primitives.map(base + "0$mc" + _ + "$sp") // Function0 + } ++ { + for (a <- ijfd; b <- ijfd) yield base + "1$mc" + a + b + "$sp" // Function1 + } ++ { + for (a <- ijd; b <- ijd; c <- ijd) yield base + "2$mc" + a + b + c + "$sp" // Function2 + } + } + + private val srJFunctionTypes: Set[InternalName] = buildFunctionTypes("scala/runtime/java8/JFunction") + def isrJFunctionType(internalName: InternalName): Boolean = srJFunctionTypes(internalName) + + private val sFunctionTypes: Set[InternalName] = buildFunctionTypes("scala/Function") + def isScalaFunctionType(internalName: InternalName): Boolean = sFunctionTypes(internalName) + implicit class AnalyzerExtensions[V <: Value](val analyzer: Analyzer[V]) extends AnyVal { def frameAt(instruction: AbstractInsnNode, methodNode: MethodNode): Frame[V] = analyzer.getFrames()(methodNode.instructions.indexOf(instruction)) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index f1392f7b7c..5ad6910d5e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -7,44 +7,102 @@ package scala.tools.nsc package backend.jvm package opt -import scala.annotation.switch -import scala.tools.asm.{Type, ClassWriter, MethodWriter, Opcodes} +import scala.annotation.{tailrec, switch} +import scala.tools.asm.Type +import scala.tools.asm.tree.analysis.BasicInterpreter import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ +import scala.collection.{mutable, immutable} import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.analysis._ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ /** - * Optimizations within a single method. + * Optimizations within a single method. Certain optimizations enable others, for example removing + * unreachable code can render a `try` block empty and enable removeEmptyExceptionHandlers. The + * latter in turn enables more unreachable code to be eliminated (the `catch` block), so there is + * a cyclic dependency. Optimizations that depend on each other are therefore executed in a loop + * until reaching a fixpoint. * - * unreachable code - * - removes instructions of basic blocks to which no branch instruction points - * + enables eliminating some exception handlers and local variable descriptors - * > eliminating them is required for correctness, as explained in `removeUnreachableCode` + * The optimizations marked UPSTREAM enable optimizations that were already executed, so they cause + * another iteration in the fixpoint loop. * - * empty exception handlers - * - removes exception handlers whose try block is empty - * + eliminating a handler where the try block is empty and reachable will turn the catch block - * unreachable. in this case "unreachable code" is invoked recursively until reaching a fixpoint. - * > for try blocks that are unreachable, "unreachable code" removes also the instructions of the - * catch block, and the recursive invocation is not necessary. + * unreachable code / DCE (removes instructions of basic blocks to which there is no branch) + * + enables downstream: + * - stale stores (loads may be eliminated, removing consumers of a store) + * - empty handlers (try blocks may become empty) + * - simplify jumps (goto l; [dead code]; l: ..) => remove goto + * - stale local variable descriptors * - * simplify jumps - * - various simplifications, see doc comments of individual optimizations - * + changing or eliminating jumps may render some code unreachable, therefore "simplify jumps" is - * executed in a loop with "unreachable code" + * note that eliminating empty handlers and stale local variable descriptors is required for + * correctness, see the comment in the body of `methodOptimizations`. * - * empty local variable descriptors - * - removes entries from the local variable table where the variable is not actually used - * + enables eliminating labels that the entry points to (if they are not otherwise referenced) + * copy propagation (replaces LOAD n to the LOAD m for the smallest m that is an alias of n) + * + enables downstrem: + * - stale stores (a stored value may not be loaded anymore) + * - store-load pairs (a load n may now be right after a store n) + * + NOTE: copy propagation is only executed once, in the first fixpoint loop iteration. none of + * the other optimizations enables further copy prop. we still run it as part of the loop + * because it requires unreachable code to be eliminated. * - * empty line numbers - * - eliminates line number nodes that describe no executable instructions - * + enables eliminating the label of the line number node (if it's not otherwise referenced) + * stale stores (replace STORE by POP) + * + enables downstream: + * - push-pop (the new pop may be the single consumer for an instruction) * - * stale labels - * - eliminate labels that are not referenced, merge sequences of label definitions. + * push-pop (when a POP is the only consumer of a value, remove the POP and its producer) + * + eanbles UPSTREAM: + * - stale stores (if a LOAD is removed, a corresponding STORE may become stale) + * + enables downstream: + * - store-load pairs (a variable may become non-live) + * - stale handlers (push-pop removes code) + * - simplify jumps (push-pop removes code) + * + * store-load pairs (remove `STORE x; LOAD x` if x is otherwise not used in the method) + * + enables downstream: + * - empty handlers (code is removes, a try block may become empty + * - simplify jumps (code is removed, a goto may become redundant for example) + * - stale local variable descriptors + * + * empty handlers (removes exception handlers whose try block is empty) + * + enables UPSTREAM: + * - unreachable code (catch block becomes unreachable) + * + enables downstream: + * - simplify jumps + * + * simplify jumps (various, like `GOTO l; l: ...`, see doc comments of individual optimizations) + * + enables UPSTREAM + * - unreachable code (`GOTO a; a: GOTO b; b: ...`, the first jump is changed to `GOTO b`, the second becomes unreachable) + * - store-load pairs (a `GOTO l; l: ...` is removed between store and load) + * + * + * The following cleanup optimizations don't enable any upstream optimizations, so they can be + * executed once at the end, when the above optimizations reach a fixpoint. + * + * + * empty local variable descriptors (removes unused variables from the local variable table) + * + enables downstream: + * - stale labels (labels that the entry points to, if not otherwise referenced) + * + * empty line numbers (eliminates line number nodes that describe no executable instructions) + * + enables downstream: + * - stale labels (label of the line number node, if not otherwise referenced) + * + * stale labels (eliminate labels that are not referenced, merge sequences of label definitions) + * + * + * Note on a method's maxLocals / maxStack: the backend only uses those values for running + * Analyzers. The values can be conservative approximations: if an optimization removes code and + * the maximal stack size is now smaller, the larger maxStack value will still work fine for + * running an Analyzer (just that frames allocate more space than required). The correct max + * values written to the bytecode are re-computed during classfile serialization. + * To keep things simpler, we don't update the max values in every optimization: + * - we do it in `removeUnreachableCodeImpl`, because it's quite straightforward + * - maxLocals is updated in `compactLocalVariables`, which runs at the end of method optimizations + * + * + * Note on updating the call graph: whenever an optimization eliminates a callsite or a closure + * instantiation, we eliminate the corresponding entry from the call graph. */ class LocalOpt[BT <: BTypes](val btypes: BT) { import LocalOptImpls._ @@ -61,6 +119,9 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { * @return A set containing the eliminated instructions */ def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = { + // In principle, for the inliner, a single removeUnreachableCodeImpl would be enough. But that + // would potentially leave behind stale handlers (empty try block) which is not legal in the + // classfile. So we run both removeUnreachableCodeImpl and removeEmptyExceptionHandlers. if (method.instructions.size == 0) return false // fast path for abstract methods if (unreachableCodeEliminated(method)) return false // we know there is no unreachable code if (!AsmAnalyzer.sizeOKForBasicValue(method)) return false // the method is too large for running an analyzer @@ -97,15 +158,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { } /** - * Remove unreachable code from a method. - * - * We rely on dead code elimination provided by the ASM framework, as described in the ASM User - * Guide (http://asm.ow2.org/index.html), Section 8.2.1. It runs a data flow analysis, which only - * computes Frame information for reachable instructions. Instructions for which no Frame data is - * available after the analysis are unreachable. - * - * Also simplifies branching instructions, removes unused local variable descriptors, empty - * exception handlers, unnecessary label declarations and empty line number nodes. + * Run method-level optimizations, see comment on class [[LocalOpt]]. * * Returns `true` if the bytecode of `method` was changed. */ @@ -138,28 +191,53 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // This triggers "ClassFormatError: Illegal exception table range in class file C". Similar // for local variables in dead blocks. Maybe that's a bug in the ASM framework. - def removalRound(): Boolean = { - // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) - val (codeRemoved, handlersRemoved, liveHandlerRemoved) = { - val (insnsRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) - val removedHandlers = removeEmptyExceptionHandlers(method) - (insnsRemoved, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) - } + /** + * Runs the optimizations that depend on each other in a loop until reaching a fixpoint. See + * comment in class [[LocalOpt]]. + */ + def removalRound(runDCE: Boolean, runCopyProp: Boolean, maxRecursion: Int = 10): Boolean = { + if (maxRecursion == 0) return false + + val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) + + // runCopyProp is only true in the first iteration; there's no optimization below that enables + // further copy propagation. it is still part of the fixpoint loop because it needs to run after DCE. + val copyPropChanged = if (runCopyProp) { + if (!runDCE) minimalRemoveUnreachableCode(method, ownerClassName) // copyProp (AliasingAnalyzer) requires DCE + copyProp(method, ownerClassName) + } else false + + val storesRemoved = if (compilerSettings.YoptCopyPropagation) eliminateStaleStores(method, ownerClassName) else false + + val pushPopRemoved = if (compilerSettings.YoptCopyPropagation) eliminatePushPop(method, ownerClassName) else false + + val storeLoadRemoved = if (compilerSettings.YoptCopyPropagation) eliminateStoreLoad(method) else false + + val removedHandlers = if (runDCE) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] + val handlersRemoved = removedHandlers.nonEmpty + val liveHandlerRemoved = removedHandlers.exists(h => liveLabels(h.start)) val jumpsChanged = if (compilerSettings.YoptSimplifyJumps) simplifyJumps(method) else false - // Eliminating live handlers and simplifying jump instructions may render more code - // unreachable, so we need to run another round. - if (liveHandlerRemoved || jumpsChanged) removalRound() + // See doc comment in the beginning of this file (optimizations marked UPSTREAM) + if (liveHandlerRemoved || jumpsChanged) { + // changing live handlers or jumps enables DCE to remove more code, so runDCE = true + removalRound(runDCE = true, runCopyProp = false, maxRecursion = maxRecursion - 1) + } else if (pushPopRemoved) { + // pushPop doesn't enable DCE, but other optimizations (stale stores). so we iterate, but without DCE. + removalRound(runDCE = false, runCopyProp = false, maxRecursion = maxRecursion - 1) + } - codeRemoved || handlersRemoved || jumpsChanged + codeRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged } - val codeHandlersOrJumpsChanged = if (compilerSettings.YoptUnreachableCode && AsmAnalyzer.sizeOKForBasicValue(method)) { + val dceCopyPropHandlersOrJumpsChanged = if (AsmAnalyzer.sizeOKForBasicValue(method)) { // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE // here is more thorough than `minimalRemoveUnreachableCode` - val r = removalRound() - unreachableCodeEliminated += method + val r = removalRound( + runDCE = compilerSettings.YoptUnreachableCode, + runCopyProp = compilerSettings.YoptCopyPropagation) + if (compilerSettings.YoptUnreachableCode) unreachableCodeEliminated += method r } else false @@ -179,9 +257,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations) assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) - unreachableCodeEliminated += method - - codeHandlersOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + dceCopyPropHandlersOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved } /** @@ -235,7 +311,641 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { method.maxStack = maxStack (changed, liveLabels) } + + /** + * For every `xLOAD n`, find all local variable slots that are aliases of `n` using an + * AliasingAnalyzer and change the instruction to `xLOAD m` where `m` is the smallest alias. + * This leaves behind potentially stale `xSTORE n` instructions, which are then eliminated + * by [[eliminateStaleStores]]. + */ + def copyProp(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForAliasing(method) && { + var changed = false + val numParams = parametersSize(method) + lazy val aliasAnalysis = new AsmAnalyzer(method, owner, new AliasingAnalyzer(new BasicInterpreter)) + + // Remember locals that are used in a `LOAD` instruction. Assume a program has two LOADs: + // + // ... + // LOAD 3 // aliases of 3 here: <3> + // ... + // LOAD 1 // aliases of 1 here: <1, 3> + // + // In this example, we should change the second load from 1 to 3, which might render the + // local variable 1 unused. + val knownUsed = new Array[Boolean](method.maxLocals) + + def usedOrMinAlias(it: IntIterator, init: Int): Int = { + if (knownUsed(init)) init + else { + var r = init + while (it.hasNext) { + val n = it.next() + // knownUsed.lenght is the number of locals, `n` may be a stack slot + if (n < knownUsed.length && knownUsed(n)) return n + if (n < r) r = n + } + r + } + } + + val it = method.instructions.iterator + while (it.hasNext) it.next() match { + case vi: VarInsnNode if vi.`var` >= numParams && isLoad(vi) => + val aliases = aliasAnalysis.frameAt(vi).asInstanceOf[AliasingFrame[_]].aliasesOf(vi.`var`) + if (aliases.size > 1) { + val alias = usedOrMinAlias(aliases.iterator, vi.`var`) + if (alias != -1) { + changed = true + vi.`var` = alias + } + } + knownUsed(vi.`var`) = true + + case _ => + } + + changed + } + } + + /** + * Eliminate `xSTORE` instructions that have no consumer. If the instruction can be completely + * eliminated, it is replaced by a POP. The [[eliminatePushPop]] cleans up unnecessary POPs. + * + * Note that an `ASOTRE` can not always be eliminated: it removes a reference to the object that + * is currently stored in that local, which potentially frees it for GC (SI-5313). Therefore + * we replace such stores by `POP; ACONST_NULL; ASTORE x`. + */ + def eliminateStaleStores(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + lazy val prodCons = new ProdConsAnalyzer(method, owner) + def hasNoCons(varIns: AbstractInsnNode, slot: Int) = prodCons.consumersOfValueAt(varIns.getNext, slot).isEmpty + + var changed = false + + // insns to delete: IINC that have no consumer + val toDelete = mutable.ArrayBuffer.empty[IincInsnNode] + // xSTORE insns to be replaced by POP or POP2 + val storesToDrop = mutable.ArrayBuffer.empty[VarInsnNode] + // ASTORE insn that have no consumer. + // - if the local is not live, the store is replaced by POP + // - otherwise, pop the argument value and store NULL instead. Unless the boolean field is + // `true`: then the store argument is already known to be ACONST_NULL. + val toNullOut = mutable.ArrayBuffer.empty[(VarInsnNode, Boolean)] + // `true` for variables that are known to be live + val liveVars = new Array[Boolean](method.maxLocals) + + val it = method.instructions.iterator + while (it.hasNext) it.next() match { + case vi: VarInsnNode if isStore(vi) && hasNoCons(vi, vi.`var`) => + val canElim = vi.getOpcode != ASTORE || { + val currentFieldValueProds = prodCons.initialProducersForValueAt(vi, vi.`var`) + currentFieldValueProds.size == 1 && (currentFieldValueProds.head match { + case ParameterProducer(0) => !isStaticMethod(method) // current field value is `this`, which won't be gc'd anyway + case _: UninitializedLocalProducer => true // field is not yet initialized, so current value cannot leak + case _ => false + }) + } + if (canElim) storesToDrop += vi + else { + val prods = prodCons.producersForValueAt(vi, prodCons.frameAt(vi).stackTop) + val isStoreNull = prods.size == 1 && prods.head.getOpcode == ACONST_NULL + toNullOut += ((vi, isStoreNull)) + } + changed = true + + case ii: IincInsnNode if hasNoCons(ii, ii.`var`) => + toDelete += ii + changed = true + + case vi: VarInsnNode => + val longSize = if (isSize2LoadOrStore(vi.getOpcode)) 1 else 0 + liveVars(vi.`var`) = true + + case ii: IincInsnNode => + liveVars(ii.`var`) = true + case _ => + } + + def replaceByPop(vi: VarInsnNode): Unit = { + val size = if (isSize2LoadOrStore(vi.getOpcode)) 2 else 1 + method.instructions.set(vi, getPop(size)) + } + + toDelete foreach method.instructions.remove + storesToDrop foreach replaceByPop + for ((vi, isStoreNull) <- toNullOut) { + if (!liveVars(vi.`var`)) replaceByPop(vi) // can drop `ASTORE x` where x has only dead stores + else { + if (!isStoreNull) { + val prev = vi.getPrevious + method.instructions.insert(prev, new InsnNode(ACONST_NULL)) + method.instructions.insert(prev, getPop(1)) + } + } + } + + changed + } + } + + /** + * When a POP instruction has a single producer, remove the POP and eliminate the producer by + * bubbling up the POPs. For example, given + * ILOAD 1; ILOAD 2; IADD; POP + * we first eliminate the POP, then the IADD, then its inputs, so the entire sequence goes away. + * If a producer cannot be eliminated (need to keep side-effects), a POP is inserted. + * + * A special case eliminates the creation of unused objects with side-effect-free constructors: + * NEW scala/Tuple1; DUP; ALOAD 0; INVOKESPECIAL scala/Tuple1.; POP + * The POP has a signle producer (the DUP), it's easy to eliminate these two. A special case + * is needed to eliminate the INVOKESPECIAL and NEW. + */ + def eliminatePushPop(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + // A queue of instructions producing a value that has to be eliminated. If possible, the + // instruction (and its inputs) will be removed, otherwise a POP is inserted after + val queue = mutable.Queue.empty[ProducedValue] + // Contains constructor invocations for values that can be eliminated if unused. + val sideEffectFreeConstructorCalls = mutable.ArrayBuffer.empty[MethodInsnNode] + + // instructions to remove (we don't change the bytecode while analyzing it. this allows + // running the ProdConsAnalyzer only once.) + val toRemove = mutable.Set.empty[AbstractInsnNode] + // instructions to insert before some instruction + val toInsertBefore = mutable.Map.empty[AbstractInsnNode, List[InsnNode]] + // an instruction to insert after some instruction + val toInsertAfter = mutable.Map.empty[AbstractInsnNode, AbstractInsnNode] + + lazy val prodCons = new ProdConsAnalyzer(method, owner) + + /** + * True if the values produced by `prod` are all the same. Most instructions produce a single + * value. DUP and DUP2 (with a size-2 input) produce two equivalent values. However, there + * are some exotic instructions that produce multiple non-equal values (DUP_X1, SWAP, ...). + * + * Assume we have `DUP_X2; POP`. In order to remove the `POP` we need to change the DUP_X2 + * into something else, which is not straightforward. + * + * Since scalac never emits any of those exotic bytecodes, we don't optimize them. + */ + def producerHasSingleOutput(prod: AbstractInsnNode): Boolean = (prod.getOpcode: @switch) match { + case DUP_X1 | DUP_X2 | DUP2_X1 | DUP2_X2 | SWAP => false + case DUP2 => prodCons.frameAt(prod).peekStack(0).getSize == 2 + case _ => true + } + + // Set.empty if not a single consumer, or if the producer is the exception value of a catch block + /** + * Returns the producers for the stack value `inputSlot` consumed by `cons`, if the consumer + * instruction is the only consumer for all of these producers. + * + * I a producer has multiple consumers, or the value is the caught exception in a catch + * block, this method returns Set.empty. + */ + def producersIfSingleConsumer(cons: AbstractInsnNode, inputSlot: Int): Set[AbstractInsnNode] = { + val prods = prodCons.producersForValueAt(cons, inputSlot) + val singleConsumer = prods forall { + case _: ExceptionProducer[_] => + false // POP of an exception in a catch block cannot be removed + case prod => + producerHasSingleOutput(prod) && { + // for DUP / DUP2, we only consider the value that is actually consumed by cons + val conss = prodCons.consumersOfValueAt(prod.getNext, inputSlot) + conss.size == 1 && conss.head == cons + } + } + if (singleConsumer) prods else Set.empty + } + + /** + * For a POP instruction that is the single consumer of its producers, remove the POP and + * enqueue the producers. + */ + def handleInitialPop(pop: AbstractInsnNode): Unit = { + val prods = producersIfSingleConsumer(pop, prodCons.frameAt(pop).stackTop) + if (prods.nonEmpty) { + toRemove += pop + val size = if (pop.getOpcode == POP2) 2 else 1 + queue ++= prods.map(ProducedValue(_, size)) + } + } + + /** + * Traverse the method in its initial state and collect all POP instructions and side-effect + * free constructor invocations that can be eliminated. + */ + def collectInitialPopsAndPureConstrs(): Unit = { + val it = method.instructions.iterator + while (it.hasNext) { + val insn = it.next() + (insn.getOpcode: @switch) match { + case POP | POP2 => + handleInitialPop(insn) + + case INVOKESPECIAL => + val mi = insn.asInstanceOf[MethodInsnNode] + if (isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi + + case _ => + } + } + } + + /** + * Eliminate the `numArgs` inputs of the instruction `prod` (which was eliminated). Fo + * each input value + * - if the `prod` instruction is the single consumer, enqueue the producers of the input + * - otherwise, insert a POP instruction to POP the input value + */ + def handleInputs(prod: AbstractInsnNode, numArgs: Int): Unit = { + val frame = prodCons.frameAt(prod) + val pops = mutable.ListBuffer.empty[InsnNode] + @tailrec def handle(stackOffset: Int): Unit = { + if (stackOffset >= 0) { + val prods = producersIfSingleConsumer(prod, frame.stackTop - stackOffset) + val nSize = frame.peekStack(stackOffset).getSize + if (prods.isEmpty) pops append getPop(nSize) + else queue ++= prods.map(ProducedValue(_, nSize)) + handle(stackOffset - 1) + } + } + handle(numArgs - 1) // handle stack offsets (numArgs - 1) to 0 + if (pops.nonEmpty) toInsertBefore(prod) = pops.toList + } + + /** + * Eliminate the closure value produced by `indy`. If the SAM type is known to construct + * without side-effects (e.g. scala/runtime/java8/JFunctionN), the `indy` and its inputs + * are eliminated, otherwise a POP is inserted. + */ + def handleClosureInst(indy: InvokeDynamicInsnNode): Unit = { + if (isrJFunctionType(Type.getReturnType(indy.desc).getInternalName)) { + toRemove += indy + callGraph.removeClosureInstantiation(indy, method) + handleInputs(indy, Type.getArgumentTypes(indy.desc).length) + } else { + toInsertAfter(indy) = getPop(1) + } + } + + def runQueue(): Unit = while (queue.nonEmpty) { + val ProducedValue(prod, size) = queue.dequeue() + + def prodString = s"Producer ${AsmUtils textify prod}@${method.instructions.indexOf(prod)}\n${AsmUtils textify method}" + def popAfterProd(): Unit = { + toInsertAfter(prod) = getPop(size) } + + (prod.getOpcode: @switch) match { + case ACONST_NULL | ICONST_M1 | ICONST_0 | ICONST_1 | ICONST_2 | ICONST_3 | ICONST_4 | ICONST_5 | LCONST_0 | LCONST_1 | FCONST_0 | FCONST_1 | FCONST_2 | DCONST_0 | DCONST_1 | + BIPUSH | SIPUSH | ILOAD | LLOAD | FLOAD | DLOAD | ALOAD | DUP => + toRemove += prod + + case DUP2 => + assert(size == 2, s"DUP2 for two size-1 values; $prodString") // ensured in method `producerHasSingleOutput` + toRemove += prod + + case DUP_X1 | DUP_X2 | DUP2_X1 | DUP2_X2 | SWAP => + // these are excluded in method `producerHasSingleOutput` + assert(false, s"Cannot eliminate value pushed by an instruction with multiple output values; $prodString") + + case IDIV | LDIV | IREM | LREM => + popAfterProd() // keep potential division by zero + + case IADD | LADD | FADD | DADD | ISUB | LSUB | FSUB | DSUB | IMUL | LMUL | FMUL | DMUL | FDIV | DDIV | FREM | DREM | + LSHL | LSHR | LUSHR | + IAND | IOR | IXOR | LAND | LOR | LXOR | + LCMP | FCMPL | FCMPG | DCMPL | DCMPG=> + toRemove += prod + handleInputs(prod, 2) + + case INEG | LNEG | FNEG | DNEG | + I2L | I2F | I2D | L2I | L2F | L2D | F2I | F2L | F2D | D2I | D2L | D2F | I2B | I2C | I2S => + toRemove += prod + handleInputs(prod, 1) + + case GETFIELD | GETSTATIC => + // TODO eliminate side-effect free module loads (https://github.com/scala/scala-dev/issues/16) + if (isBoxedUnit(prod)) toRemove += prod + else popAfterProd() // keep potential class initialization (static field) or NPE (instance field) + + case INVOKEVIRTUAL | INVOKESPECIAL | INVOKESTATIC | INVOKEINTERFACE => + val methodInsn = prod.asInstanceOf[MethodInsnNode] + if (isSideEffectFreeCall(methodInsn)) { + toRemove += prod + callGraph.removeCallsite(methodInsn, method) + val receiver = if (methodInsn.getOpcode == INVOKESTATIC) 0 else 1 + handleInputs(prod, Type.getArgumentTypes(methodInsn.desc).length + receiver) + } else + popAfterProd() + + case INVOKEDYNAMIC => + prod match { + case callGraph.LambdaMetaFactoryCall(indy, _, _, _) => handleClosureInst(indy) + case _ => popAfterProd() + } + + case CHECKCAST => + // Special case for `IndyLambda JFunctionN; CHECKCAST FuncionN`. This sequence is emitted + // for all closure instantiations, see https://issues.scala-lang.org/browse/SI-9540 + val castType = prod.asInstanceOf[TypeInsnNode].desc + if (isScalaFunctionType(castType)) { + + // the `FunctionN` part of an internal name (e.g. `scala/runtime/java8/JFunction1$mcDI$sp`) + def funPart(funType: String): String = { + var end = funType.indexOf('$') + if (end == -1) end = funType.length + funType.substring(funType.lastIndexOf('/') + 1, end) + } + + // true if the cast will always succeed + def castOk(indy: InvokeDynamicInsnNode): Boolean = { + val generatedFunctionType = Type.getReturnType(indy.desc).getInternalName + isrJFunctionType(generatedFunctionType) && funPart(generatedFunctionType) == "J" + funPart(castType) + } + + val castProd = producersIfSingleConsumer(prod, prodCons.frameAt(prod).stackTop) + if (castProd.size == 1) castProd.head match { + case callGraph.LambdaMetaFactoryCall(indy, _, _, _) if castOk(indy) => + toRemove += prod // remove the cast + handleClosureInst(indy) // remove the indyLambda + case _ => popAfterProd() + } else + popAfterProd() + } else + popAfterProd() + + case NEW => + if (isNewForSideEffectFreeConstructor(prod)) toRemove += prod + else popAfterProd() + + case LDC => + // don't remove class literals: keep the potential NoClassDefFoundError + if (prod.asInstanceOf[LdcInsnNode].cst.isInstanceOf[Type]) popAfterProd() + else toRemove += prod + + case NEWARRAY | ANEWARRAY => + toRemove += prod + handleInputs(prod, 1) + + case MULTIANEWARRAY => + toRemove += prod + handleInputs(prod, prod.asInstanceOf[MultiANewArrayInsnNode].dims) + + case _ => + popAfterProd() + } + } + + // there are two cases when we can eliminate a constructor call: + // - NEW T; INVOKESPECIAL T. -- there's no DUP, the new object is consumed only by the constructor) + // - NEW T; DUP; INVOKESPECIAL T., where the DUP will be removed + def eliminateUnusedPureConstructorCalls(): Boolean = { + var changed = false + + def removeConstructorCall(mi: MethodInsnNode): Unit = { + toRemove += mi + callGraph.removeCallsite(mi, method) + sideEffectFreeConstructorCalls -= mi + changed = true + } + + for (mi <- sideEffectFreeConstructorCalls.toList) { // toList to allow removing elements while traversing + val frame = prodCons.frameAt(mi) + val stackTop = frame.stackTop + val numArgs = Type.getArgumentTypes(mi.desc).length + val receiverProds = producersIfSingleConsumer(mi, stackTop - numArgs) + if (receiverProds.size == 1) { + val receiverProd = receiverProds.head + if (receiverProd.getOpcode == NEW) { + removeConstructorCall(mi) + handleInputs(mi, numArgs + 1) // removes the producers of args and receiver + } else if (receiverProd.getOpcode == DUP && toRemove.contains(receiverProd)) { + val dupProds = producersIfSingleConsumer(receiverProd, prodCons.frameAt(receiverProd).stackTop) + if (dupProds.size == 1 && dupProds.head.getOpcode == NEW) { + removeConstructorCall(mi) + handleInputs(mi, numArgs) // removes the producers of args. the producer of the receiver is DUP and already in toRemove. + queue += ProducedValue(dupProds.head, 1) // removes the NEW (which is NOT the producer of the receiver!) + } + } + } + } + changed + } + + collectInitialPopsAndPureConstrs() + + // eliminating producers enables eliminating unused constructor calls (when a DUP gets removed). + // vice-versa, eliminating a constructor call adds producers of constructor parameters to the queue. + // so the two run in a loop. + runQueue() + while(eliminateUnusedPureConstructorCalls()) + runQueue() + + var changed = false + toInsertAfter foreach { + case (target, insn) => + nextExecutableInstructionOrLabel(target) match { + // `insn` is of type `InsnNode`, so we only need to check the Opcode when comparing to another instruction + case Some(next) if next.getOpcode == insn.getOpcode && toRemove(next) => + // Inserting and removing a POP at the same place should not enable `changed`. This happens + // when a POP directly follows a producer that cannot be eliminated, e.g. INVOKESTATIC A.m ()I; POP + // The POP is initially added to `toRemove`, and the `INVOKESTATIC` producer is added to the queue. + // Because the producer cannot be elided, a POP is added to `toInsertAfter`. + toRemove -= next + + case _ => + changed = true + method.instructions.insert(target, insn) + } + } + toInsertBefore foreach { + case (target, insns) => + changed = true + insns.foreach(method.instructions.insertBefore(target, _)) + } + toRemove foreach { insn => + changed = true + method.instructions.remove(insn) + } + changed + } + } + + case class ProducedValue(producer: AbstractInsnNode, size: Int) { + override def toString = s"<${AsmUtils textify producer}>" + } + + /** + * Remove `xSTORE n; xLOAD n` paris if + * - the local variable n is not used anywhere else in the method (1), and + * - there are no executable instructions and no live labels (jump targets) between the two (2) + * + * Note: store-load pairs that cannot be eliminated could be replaced by `DUP; xSTORE n`, but + * that's just cosmetic and doesn't help for anything. + * + * (1) This could be made more precise by running a prodCons analysis and checking that the load + * is the only user of the store. Then we could eliminate the pair even if the variable is live + * (except for ASTORE, SI-5313). Not needing an analyzer is more efficient, and catches most + * cases. + * + * (2) The implementation uses a conservative estimation for liveness (if some instruction uses + * local n, then n is considered live in the entire method). In return, it doesn't need to run an + * Analyzer on the method, making it more efficient. + * + * This method also removes `ACONST_NULL; ASTORE n` if the local n is not live. This pattern is + * introduced by [[eliminateStaleStores]]. + * + * The implementation is a little tricky to support the following case: + * ISTORE 1; ISTORE 2; ILOAD 2; ACONST_NULL; ASTORE 3; ILOAD 1 + * The outer store-load pair can be removed if two the inner pairs can be. + */ + def eliminateStoreLoad(method: MethodNode): Boolean = { + val removePairs = mutable.Set.empty[RemovePair] + val liveVars = new Array[Boolean](method.maxLocals) + val liveLabels = mutable.Set.empty[LabelNode] + + def mkRemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]): RemovePair = { + val r = RemovePair(store, other, depends) + removePairs += r + r + } + + def registerLiveVarsLabels(insn: AbstractInsnNode): Unit = insn match { + case vi: VarInsnNode => liveVars(vi.`var`) = true + case ii: IincInsnNode => liveVars(ii.`var`) = true + case j: JumpInsnNode => liveLabels += j.label + case s: TableSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala + case s: LookupSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala + case _ => + } + + val pairStartStack = new mutable.Stack[(AbstractInsnNode, mutable.ListBuffer[RemovePairDependency])] + + def push(insn: AbstractInsnNode) = { + pairStartStack push ((insn, mutable.ListBuffer.empty)) + } + + def addDepends(dependency: RemovePairDependency) = if (pairStartStack.nonEmpty) { + val (_, depends) = pairStartStack.top + depends += dependency + } + + def completesStackTop(load: AbstractInsnNode) = isLoad(load) && pairStartStack.nonEmpty && { + pairStartStack.top match { + case (store: VarInsnNode, _) => store.`var` == load.asInstanceOf[VarInsnNode].`var` + case _ => false + } + } + + /** + * Try to pair `insn` with its correspondant on the stack + * - if the stack top is a store and `insn` is a corresponding load, create a pair + * - otherwise, check the two top stack values for `null; store`. if it matches, create + * a pair and continue pairing `insn` on the remaining stack + * - otherwise, empty the stack and mark the local variables in it live + */ + def tryToPairInstruction(insn: AbstractInsnNode): Unit = { + @tailrec def emptyStack(): Unit = if (pairStartStack.nonEmpty) { + registerLiveVarsLabels(pairStartStack.pop()._1) + emptyStack() + } + + @tailrec def tryPairing(): Unit = { + if (completesStackTop(insn)) { + val (store: VarInsnNode, depends) = pairStartStack.pop() + addDepends(mkRemovePair(store, insn, depends.toList)) + } else if (pairStartStack.nonEmpty) { + val (top, topDepends) = pairStartStack.pop() + if (pairStartStack.nonEmpty) { + (pairStartStack.top, top) match { + case ((ldNull: InsnNode, depends), store: VarInsnNode) if ldNull.getOpcode == ACONST_NULL && store.getOpcode == ASTORE => + pairStartStack.pop() + addDepends(mkRemovePair(store, ldNull, depends.toList)) + // example: store; (null; store;) (store; load;) load + // s1^ ^^^^^p1^^^^^ // p1 is added to s1's depends + // then: store; (null; store;) load + // s2^ ^^^^p2^^^^^ // p1 and p2 are added to s2's depends + topDepends foreach addDepends + tryPairing() + + case _ => + // empty the stack - a non-matching insn was found, cannot create any pairs to remove + registerLiveVarsLabels(insn) + registerLiveVarsLabels(top) + emptyStack() + } + } else { + // stack only has one element + registerLiveVarsLabels(insn) + registerLiveVarsLabels(top) + } + } else { + // stack is empty already + registerLiveVarsLabels(insn) + } + } + + tryPairing() + } + + + var insn = method.instructions.getFirst + + @tailrec def advanceToNextExecutableOrLabel(): Unit = { + insn = insn.getNext + if (insn != null && !isExecutable(insn) && !insn.isInstanceOf[LabelNode]) advanceToNextExecutableOrLabel() + } + + while (insn != null) { + insn match { + case _ if insn.getOpcode == ACONST_NULL => push(insn) + case vi: VarInsnNode if isStore(vi) => push(insn) + case label: LabelNode if pairStartStack.nonEmpty => addDepends(LabelNotLive(label)) + case _ => tryToPairInstruction(insn) + } + advanceToNextExecutableOrLabel() + } + + // elide RemovePairs that depend on live labels or other RemovePair that have to be elided. + // example: store 1; store 2; label x; load 2; load 1 + // if x is live, the inner pair has to be elided, causing the outer pair to be elided too. + + var doneEliding = false + + def elide(removePair: RemovePair) = { + doneEliding = false + liveVars(removePair.store.`var`) = true + removePairs -= removePair + } + + while (!doneEliding) { + doneEliding = true + for (removePair <- removePairs.toList) { + val slot = removePair.store.`var` + if (liveVars(slot)) elide(removePair) + else removePair.depends foreach { + case LabelNotLive(label) => if (liveLabels(label)) elide(removePair) + case other: RemovePair => if (!removePairs(other)) elide(removePair) + } + } + } + + for (removePair <- removePairs) { + method.instructions.remove(removePair.store) + method.instructions.remove(removePair.other) + } + + removePairs.nonEmpty + } +} + +trait RemovePairDependency +case class RemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]) extends RemovePairDependency { + override def toString = s"<${AsmUtils textify store},${AsmUtils textify other}> [$depends]" } +case class LabelNotLive(label: LabelNode) extends RemovePairDependency object LocalOptImpls { /** @@ -550,7 +1260,7 @@ object LocalOptImpls { private def simplifyBranchOverGoto(method: MethodNode, instruction: AbstractInsnNode): Option[JumpInsnNode] = instruction match { case ConditionalJump(jump) => // don't skip over labels, see doc comment - nextExecutableInstruction(jump, alsoKeep = _.isInstanceOf[LabelNode]) match { + nextExecutableInstructionOrLabel(jump) match { case Some(Goto(goto)) => if (nextExecutableInstruction(goto, alsoKeep = Set(jump.label)) == Some(jump.label)) { val newJump = new JumpInsnNode(negateJumpOpcode(jump.getOpcode), goto.label) diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index a9ec8c7b30..cbd9ee1a55 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -230,6 +230,7 @@ trait ScalaSettings extends AbsScalaSettings val emptyLineNumbers = Choice("empty-line-numbers", "Eliminate unnecessary line number information.") val emptyLabels = Choice("empty-labels", "Eliminate and collapse redundant labels in the bytecode.") val compactLocals = Choice("compact-locals", "Eliminate empty slots in the sequence of local variables.") + val copyPropagation = Choice("copy-propagation", "Eliminate redundant local variables (locals that are copies of others).") val nullnessTracking = Choice("nullness-tracking", "Track nullness / non-nullness of local variables and apply optimizations.") val closureElimination = Choice("closure-elimination" , "Rewrite closure invocations to the implementation method and eliminate closures.") val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled.") @@ -240,7 +241,7 @@ trait ScalaSettings extends AbsScalaSettings private val defaultChoices = List(unreachableCode) val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices) - private val methodChoices = List(unreachableCode, simplifyJumps, emptyLineNumbers, emptyLabels, compactLocals, nullnessTracking, closureElimination) + private val methodChoices = List(unreachableCode, simplifyJumps, emptyLineNumbers, emptyLabels, compactLocals, copyPropagation, nullnessTracking, closureElimination) val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) private val projectChoices = List(lMethod, inlineProject) @@ -262,6 +263,7 @@ trait ScalaSettings extends AbsScalaSettings def YoptEmptyLineNumbers = Yopt.contains(YoptChoices.emptyLineNumbers) def YoptEmptyLabels = Yopt.contains(YoptChoices.emptyLabels) def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals) + def YoptCopyPropagation = Yopt.contains(YoptChoices.copyPropagation) def YoptNullnessTracking = Yopt.contains(YoptChoices.nullnessTracking) def YoptClosureElimination = Yopt.contains(YoptChoices.closureElimination) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala index 735bbefbbb..54724458e2 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala @@ -49,7 +49,7 @@ class ClosureOptimizerTest extends ClearAfterClass { """.stripMargin val List(c) = compileClasses(compiler)(code) - val t = c.methods.asScala.toList.find(_.name == "t").get + val t = findAsmMethod(c, "t") val List(bodyCall) = findInstr(t, "INVOKESTATIC C.C$$$anonfun$1 ()Lscala/runtime/Nothing$") assert(bodyCall.getNext.getOpcode == ATHROW) } @@ -65,7 +65,7 @@ class ClosureOptimizerTest extends ClearAfterClass { """.stripMargin val List(c) = compileClasses(compiler)(code) - val t = c.methods.asScala.toList.find(_.name == "t").get + val t = findAsmMethod(c, "t") val List(bodyCall) = findInstr(t, "INVOKESTATIC C.C$$$anonfun$1 ()Lscala/runtime/Null$") assert(bodyCall.getNext.getOpcode == POP) assert(bodyCall.getNext.getNext.getOpcode == ACONST_NULL) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 1e01627969..1037fd5221 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -23,8 +23,9 @@ import scala.collection.convert.decorateAsScala._ import scala.tools.testing.ClearAfterClass object InlinerTest extends ClearAfterClass.Clearable { - val args = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings" + val args = "-Yopt:l:classpath -Yopt-warnings" var compiler = newCompiler(extraArgs = args) + var inlineOnlyCompiler = newCompiler(extraArgs = "-Yopt:inline-project") // allows inspecting the caches after a compilation run def notPerRun: List[Clearable] = List( @@ -34,7 +35,7 @@ object InlinerTest extends ClearAfterClass.Clearable { compiler.genBCode.bTypes.callGraph.callsites) notPerRun foreach compiler.perRunCaches.unrecordCache - def clear(): Unit = { compiler = null } + def clear(): Unit = { compiler = null; inlineOnlyCompiler = null } implicit class listStringLines[T](val l: List[T]) extends AnyVal { def stringLines = l.mkString("\n") @@ -65,6 +66,8 @@ class InlinerTest extends ClearAfterClass { import compiler.genBCode.bTypes.backendUtils._ import inlinerHeuristics._ + val inlineOnlyCompiler = InlinerTest.inlineOnlyCompiler + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { InlinerTest.notPerRun.foreach(_.clear()) compileClasses(compiler)(scalaCode, javaCode, allowMessage) @@ -148,15 +151,22 @@ class InlinerTest extends ClearAfterClass { // See also discussion around ATHROW in BCodeBodyBuilder val g = inlineTest(code) - val expectedInlined = List( - VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this - Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) // inlined call to ??? - assertSameCode(convertMethod(g).instructions.dropNonOp.take(4), expectedInlined) + val invokeQQQ = List( + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), + Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) + + val gBeforeLocalOpt = VarOp(ALOAD, 0) :: VarOp(ASTORE, 1) :: invokeQQQ ::: List( + VarOp(ASTORE, 2), + Jump(GOTO, Label(11)), + Label(11), + VarOp(ALOAD, 2), + Op(ATHROW)) + + assertSameCode(convertMethod(g).instructions.dropNonOp, gBeforeLocalOpt) compiler.genBCode.bTypes.localOpt.methodOptimizations(g, "C") - assertSameCode(convertMethod(g).instructions.dropNonOp, - expectedInlined ++ List(VarOp(ASTORE, 2), VarOp(ALOAD, 2), Op(ATHROW))) + assertSameCode(convertMethod(g).instructions.dropNonOp, invokeQQQ :+ Op(ATHROW)) } @Test @@ -396,7 +406,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val List(c) = compile(code) + // use a compiler without local optimizations (cleanups) + val List(c) = compileClasses(inlineOnlyCompiler)(code) val ms @ List(f1, f2, g1, g2) = c.methods.asScala.filter(_.name.length == 2).toList // stack height at callsite of f1 is 1, so max of g1 after inlining is max of f1 + 1 @@ -959,7 +970,7 @@ class InlinerTest extends ClearAfterClass { val List(c) = compile(code) val t = getSingleMethod(c, "t").instructions assertNoInvoke(t) - assert(2 == t.collect({case Ldc(_, "hai!") => }).size) // twice the body of f + assert(1 == t.collect({case Ldc(_, "hai!") => }).size) // push-pop eliminates the first LDC("hai!") assert(1 == t.collect({case Jump(IFNONNULL, _) => }).size) // one single null check } @@ -985,17 +996,17 @@ class InlinerTest extends ClearAfterClass { val List(c, _, _) = compile(code) val t1 = getSingleMethod(c, "t1") - assert(t1.instructions exists { - case _: InvokeDynamic => true - case _ => false + assert(t1.instructions forall { // indy is eliminated by push-pop + case _: InvokeDynamic => false + case _ => true }) // the indy call is inlined into t, and the closure elimination rewrites the closure invocation to the body method assertInvoke(t1, "C", "C$$$anonfun$2") val t2 = getSingleMethod(c, "t2") - assert(t2.instructions exists { - case _: InvokeDynamic => true - case _ => false + assert(t2.instructions forall { // indy is eliminated by push-pop + case _: InvokeDynamic => false + case _ => true }) assertInvoke(t2, "M$", "M$$$anonfun$1") } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala index 8d910629ca..734b5d9172 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala @@ -8,6 +8,7 @@ import org.junit.Test import scala.tools.asm.Opcodes._ import org.junit.Assert._ +import scala.tools.nsc.backend.jvm.AsmUtils._ import scala.tools.testing.AssertUtil._ import CodeGenTools._ @@ -36,12 +37,13 @@ class MethodLevelOpts extends ClearAfterClass { } @Test - def cannotEliminateLoadBoxedUnit(): Unit = { + def eliminateLoadBoxedUnit(): Unit = { // the compiler inserts a boxed into the try block. it's therefore non-empty (and live) and not eliminated. val code = "def f = { try {} catch { case _: Throwable => 0 }; 1 }" val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.length == 1) - assertSameCode(m.instructions.take(3), List(Label(0), LineNumber(1, Label(0)), Field(GETSTATIC, "scala/runtime/BoxedUnit", "UNIT", "Lscala/runtime/BoxedUnit;"))) + assertTrue(m.handlers.length == 0) + assertSameCode(m.instructions.dropNonOp, + List(Op(ICONST_1), Op(IRETURN))) } @Test @@ -75,18 +77,162 @@ class MethodLevelOpts extends ClearAfterClass { | case _: Throwable => | return 2 | } finally { - | return 2 + | return 3 | // dead | val x = try 10 catch { case _: Throwable => 11 } | println(x) | } """.stripMargin val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.length == 2) - assertSameCode(m.instructions.dropNonOp, // drop line numbers and labels that are only used by line numbers + assertTrue(m.handlers.isEmpty) + assertSameCode(m.instructions.dropNonOp, List(Op(ICONST_3), Op(IRETURN))) + } - // one single label left :-) - List(Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(20)), Op(POP), Op(ICONST_2), VarOp(ISTORE, 2), Jump(GOTO, Label(20)), VarOp(ASTORE, 3), Op(ICONST_2), Op(IRETURN), Label(20), Op(ICONST_2), Op(IRETURN)) - ) + @Test + def nullStoreLoadElim(): Unit = { + // point of this test: we have two cleanups + // - remove `ACONST_NULL; ASTORE x` if x is otherwise not live + // - remove `ASTORE x; ALOAD x` if x is otherwise not live + // in the example below, we have `ACONST_NULL; ASTORE x; ALOAD x`. in this case the store-load + // should be removed (even though it looks like a null-store at first). + val code = + """class C { + | def t = { + | val x = null + | x.toString + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t").instructions.dropNonOp, + List(Op(ACONST_NULL), Invoke(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false), Op(ARETURN))) + } + + @Test + def deadStoreReferenceElim(): Unit = { + val code = + """class C { + | def t = { + | var a = "a" // assign to non-initialized, directly removed by dead store + | a = "b" // assign to initialized, replaced by null-store, which is then removed: the var is not live, the uses are null-store or store-load + | a = "c" + | a // store-load pair will be eliminated + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode( + getSingleMethod(c, "t").instructions.dropNonOp, + List(Ldc(LDC, "c"), Op(ARETURN))) + } + + @Test + def deadStoreReferenceKeepNull(): Unit = { + val code = + """class C { + | def t = { + | var a = "el" // this store is live, used in the println. + | println(a) + | a = "met" // since it's an ASTORE to a live variable, cannot elim the store (SI-5313), but store null instead. + | // so we get `LDC met; POP; ACONST_NULL; ASTORE 1`. the `LDC met; POP` is eliminated by push-pop. + | a = "zit" // this store is live, so we get `LDC zit; ASOTRE 1; ALOAD 1; ARETURN`. + | // we cannot eliminated the store-load sequence, because the local is live (again SI-5313). + | a + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + + assertEquals( + getSingleMethod(c, "t").instructions.dropNonOp, + List( + Ldc(LDC, "el"), VarOp(ASTORE, 1), + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/Predef$", "println", "(Ljava/lang/Object;)V", false), + Op(ACONST_NULL), VarOp(ASTORE, 1), + Ldc(LDC, "zit"), VarOp(ASTORE, 1), VarOp(ALOAD, 1), Op(ARETURN))) + } + + @Test + def elimUnusedTupleObjectStringBox(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = (x, y) // Tuple2$mcII$sp + | val b = (a, y) // Tuple2 + | val c = (new Object, "krik", new String) // unused java/lang/Object, java/lang/String allocation and string constant is also eliminated + | val d = new java.lang.Integer(x) + | val e = new String(new Array[Char](23)) + | val f = new scala.runtime.IntRef(11) + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def noElimImpureConstructor(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = new java.lang.Integer("nono") + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(TypeOp(NEW, "java/lang/Integer"), Ldc(LDC, "nono"), Invoke(INVOKESPECIAL, "java/lang/Integer", "", "(Ljava/lang/String;)V", false), + VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def elimUnusedBoxUnbox(): Unit = { + val code = + """class C { + | def t(a: Long): Int = { + | val t = 3 + a + | val u = a + t + | val v: Any = u // scala/runtime/BoxesRunTime.boxToLong + | + | val w = (v, a) // a Tuple2 (not specialized because first value is Any) + | // so calls scala/runtime/BoxesRunTime.boxToLong on the second value + | + | val x = v.asInstanceOf[Long] // scala/runtime/BoxesRunTime.unboxToLong + | + | val z = (java.lang.Long.valueOf(a), t) // java box call on the left, scala/runtime/BoxesRunTime.boxToLong on the right + | + | 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(Op(ICONST_0), Op(IRETURN))) + } + + @Test + def elimUnusedClosure(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val f = (a: Int) => a + x + y + | val g = (b: Int) => b - x + | val h = (s: String) => println(s) + | f(30) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals( + getSingleMethod(c, "t").instructions.dropNonOp, + List( + IntOp(BIPUSH, 30), VarOp(ISTORE, 3), // no constant propagation, so we keep the store (and load below) of a const + VarOp(ILOAD, 1), + VarOp(ILOAD, 2), + VarOp(ILOAD, 3), + Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(III)I", false), Op(IRETURN))) } } -- cgit v1.2.3 From 9fa3783a11079ce48afc42d8da633c0b8c9f7526 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Tue, 10 Nov 2015 11:47:38 +0100 Subject: Clean up optimizer settings. --- .../scala/tools/nsc/backend/jvm/GenBCode.scala | 2 +- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 18 +++++++++--------- .../scala/tools/nsc/settings/ScalaSettings.scala | 22 +++++++++------------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala index 35e4db33bc..b0ec37db97 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala @@ -237,7 +237,7 @@ abstract class GenBCode extends BCodeSyncAndTry { } if (settings.YoptInlinerEnabled) bTypes.inliner.runInliner() - if (settings.YoptClosureElimination) + if (settings.YoptClosureInvocations) closureOptimizer.rewriteClosureApplyInvocations() } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 5ad6910d5e..b4868a03ef 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -198,14 +198,14 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { def removalRound(runDCE: Boolean, runCopyProp: Boolean, maxRecursion: Int = 10): Boolean = { if (maxRecursion == 0) return false - val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) + // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores) + // require not having unreachable instructions (null frames). + val dceRequired = runDCE || compilerSettings.YoptCopyPropagation + val (codeRemoved, liveLabels) = if (dceRequired) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) // runCopyProp is only true in the first iteration; there's no optimization below that enables // further copy propagation. it is still part of the fixpoint loop because it needs to run after DCE. - val copyPropChanged = if (runCopyProp) { - if (!runDCE) minimalRemoveUnreachableCode(method, ownerClassName) // copyProp (AliasingAnalyzer) requires DCE - copyProp(method, ownerClassName) - } else false + val copyPropChanged = if (runCopyProp) copyProp(method, ownerClassName) else false val storesRemoved = if (compilerSettings.YoptCopyPropagation) eliminateStaleStores(method, ownerClassName) else false @@ -213,7 +213,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val storeLoadRemoved = if (compilerSettings.YoptCopyPropagation) eliminateStoreLoad(method) else false - val removedHandlers = if (runDCE) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] + val removedHandlers = if (dceRequired) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] val handlersRemoved = removedHandlers.nonEmpty val liveHandlerRemoved = removedHandlers.exists(h => liveLabels(h.start)) @@ -233,7 +233,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val dceCopyPropHandlersOrJumpsChanged = if (AsmAnalyzer.sizeOKForBasicValue(method)) { // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE - // here is more thorough than `minimalRemoveUnreachableCode` + // here is more thorough than `minimalRemoveUnreachableCode` that run before inlining. val r = removalRound( runDCE = compilerSettings.YoptUnreachableCode, runCopyProp = compilerSettings.YoptCopyPropagation) @@ -247,9 +247,9 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { else if (compilerSettings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*) else false - val lineNumbersRemoved = if (compilerSettings.YoptEmptyLineNumbers) removeEmptyLineNumbers(method) else false + val lineNumbersRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLineNumbers(method) else false - val labelsRemoved = if (compilerSettings.YoptEmptyLabels) removeEmptyLabelNodes(method) else false + val labelsRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLabelNodes(method) else false // assert that local variable annotations are empty (we don't emit them) - otherwise we'd have // to eliminate those covering an empty range, similar to removeUnusedLocalVariableNodes. diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index cbd9ee1a55..794c0ba324 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -225,23 +225,21 @@ trait ScalaSettings extends AbsScalaSettings val YskipInlineInfoAttribute = BooleanSetting("-Yskip-inline-info-attribute", "Do not add the ScalaInlineInfo attribute to classfiles generated by -Ybackend:GenASM") object YoptChoices extends MultiChoiceEnumeration { - val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code, exception handlers protecting no instructions, debug information of eliminated variables.") + val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code, exception handlers guarding no instructions, redundant metadata (debug information, line numbers).") val simplifyJumps = Choice("simplify-jumps", "Simplify branching instructions, eliminate unnecessary ones.") - val emptyLineNumbers = Choice("empty-line-numbers", "Eliminate unnecessary line number information.") - val emptyLabels = Choice("empty-labels", "Eliminate and collapse redundant labels in the bytecode.") val compactLocals = Choice("compact-locals", "Eliminate empty slots in the sequence of local variables.") - val copyPropagation = Choice("copy-propagation", "Eliminate redundant local variables (locals that are copies of others).") + val copyPropagation = Choice("copy-propagation", "Eliminate redundant local variables and unused values (including closures). Enables unreachable-code.") val nullnessTracking = Choice("nullness-tracking", "Track nullness / non-nullness of local variables and apply optimizations.") - val closureElimination = Choice("closure-elimination" , "Rewrite closure invocations to the implementation method and eliminate closures.") - val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled.") - val inlineGlobal = Choice("inline-global", "Inline methods from any source, including classfiles on the compile classpath.") + val closureInvocations = Choice("closure-invocations" , "Rewrite closure invocations to the implementation method.") + val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled. Enables unreachable-code.") + val inlineGlobal = Choice("inline-global", "Inline methods from any source, including classfiles on the compile classpath. Enables unreachable-code.") val lNone = Choice("l:none", "Don't enable any optimizations.") private val defaultChoices = List(unreachableCode) val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices) - private val methodChoices = List(unreachableCode, simplifyJumps, emptyLineNumbers, emptyLabels, compactLocals, copyPropagation, nullnessTracking, closureElimination) + private val methodChoices = List(unreachableCode, simplifyJumps, compactLocals, copyPropagation, nullnessTracking, closureInvocations) val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) private val projectChoices = List(lMethod, inlineProject) @@ -260,19 +258,17 @@ trait ScalaSettings extends AbsScalaSettings def YoptNone = Yopt.isSetByUser && Yopt.value.isEmpty def YoptUnreachableCode = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode) def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps) - def YoptEmptyLineNumbers = Yopt.contains(YoptChoices.emptyLineNumbers) - def YoptEmptyLabels = Yopt.contains(YoptChoices.emptyLabels) def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals) def YoptCopyPropagation = Yopt.contains(YoptChoices.copyPropagation) def YoptNullnessTracking = Yopt.contains(YoptChoices.nullnessTracking) - def YoptClosureElimination = Yopt.contains(YoptChoices.closureElimination) + def YoptClosureInvocations = Yopt.contains(YoptChoices.closureInvocations) def YoptInlineProject = Yopt.contains(YoptChoices.inlineProject) def YoptInlineGlobal = Yopt.contains(YoptChoices.inlineGlobal) def YoptInlinerEnabled = YoptInlineProject || YoptInlineGlobal - def YoptBuildCallGraph = YoptInlinerEnabled || YoptClosureElimination - def YoptAddToBytecodeRepository = YoptBuildCallGraph || YoptInlinerEnabled || YoptClosureElimination + def YoptBuildCallGraph = YoptInlinerEnabled || YoptClosureInvocations + def YoptAddToBytecodeRepository = YoptBuildCallGraph || YoptInlinerEnabled || YoptClosureInvocations val YoptInlineHeuristics = ChoiceSetting( name = "-Yopt-inline-heuristics", -- cgit v1.2.3 From 12b6598714d72bd1d275d756de0b8e7df04d1f16 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Tue, 10 Nov 2015 15:02:55 +0100 Subject: Fix specialized function class names in closure optimizer --- .../scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 6 ++++-- .../tools/nsc/backend/jvm/opt/MethodLevelOpts.scala | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index 3854bc840d..d7ef13cb9c 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -461,15 +461,17 @@ object BytecodeUtils { private def buildFunctionTypes(base: String): Set[InternalName] = { def primitives = Iterator("B", "S", "I", "J", "C", "F", "D", "Z", "V") def ijfd = Iterator("I", "J", "F", "D") + def ijfdzv = Iterator("I", "J", "F", "D", "Z", "V") def ijd = Iterator("I", "J", "D") Set.empty[String] ++ { (0 to 22).map(base + _) } ++ { primitives.map(base + "0$mc" + _ + "$sp") // Function0 } ++ { - for (a <- ijfd; b <- ijfd) yield base + "1$mc" + a + b + "$sp" // Function1 + // return type specializations appear first in the name string (alphabetical sorting) + for (r <- ijfdzv; a <- ijfd) yield base + "1$mc" + r + a + "$sp" // Function1 } ++ { - for (a <- ijd; b <- ijd; c <- ijd) yield base + "2$mc" + a + b + c + "$sp" // Function2 + for (r <- ijfdzv; a <- ijd; b <- ijd) yield base + "2$mc" + r + a + b + "$sp" // Function2 } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala index 734b5d9172..3228d3de65 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala @@ -235,4 +235,21 @@ class MethodLevelOpts extends ClearAfterClass { VarOp(ILOAD, 3), Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(III)I", false), Op(IRETURN))) } + + @Test + def rewriteSpecializedClosureCall(): Unit = { + val code = + """class C { + | def t = { + | val f1 = (x: Int) => println(x) // int-unit specialization + | val f2 = (x: Int, y: Long) => x == y // int-long-boolean + | f1(1) + | f2(3, 4) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + val t = getSingleMethod(c, "t") + assert(!t.instructions.exists(_.opcode == INVOKEDYNAMIC), t) + } } -- cgit v1.2.3 From d4bfc59b781b465ef980cd7082462eb2cfe2c8b8 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 13 Nov 2015 16:34:27 +0100 Subject: Support specialized functions in closure optimizer Fixes https://github.com/scala/scala-dev/issues/52. An IndyLambda may create a specialized function type, where the SAM is the corresponding specialized variant of apply. If this closure is invoked through the generic apply method, the closure optimizer would previously not re-write the invocation to the $anonfun method. This is now done, including the necessary box / unbox operations. --- .../scala/tools/nsc/backend/jvm/CoreBTypes.scala | 7 + .../nsc/backend/jvm/analysis/BackendUtils.scala | 10 + .../tools/nsc/backend/jvm/opt/CallGraph.scala | 2 +- .../nsc/backend/jvm/opt/ClosureOptimizer.scala | 207 ++++++++++++++++----- .../nsc/backend/jvm/opt/ClosureOptimizerTest.scala | 17 ++ .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 190 +++++++++++++++++++ 6 files changed, 386 insertions(+), 47 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala index 79aa4308c5..0317e08d9e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala @@ -119,6 +119,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(exitingPickler(rootMirror.getRequiredClass("java.lang.invoke.MethodHandles.Lookup"))) // didn't find a reliable non-stringly-typed way that works for inner classes in the backend lazy val srLambdaDeserializerRef : ClassBType = classBTypeFromSymbol(requiredModule[scala.runtime.LambdaDeserializer.type].moduleClass) lazy val srBoxesRunTimeRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxesRunTime]) + lazy val srBoxedUnitRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxedUnit]) lazy val hashMethodSym: Symbol = getMember(ScalaRunTimeModule, nme.hash_) @@ -202,6 +203,11 @@ trait CoreBTypesProxyGlobalIndependent[BTS <: BTypes] { def jliMethodHandlesRef : ClassBType def jliMethodHandlesLookupRef : ClassBType def srLambdaDeserializerRef : ClassBType + def srBoxesRunTimeRef : ClassBType + def srBoxedUnitRef : ClassBType + + def asmBoxTo : Map[BType, MethodNameAndType] + def asmUnboxTo: Map[BType, MethodNameAndType] } /** @@ -242,6 +248,7 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: def jliMethodHandlesLookupRef : ClassBType = _coreBTypes.jliMethodHandlesLookupRef def srLambdaDeserializerRef : ClassBType = _coreBTypes.srLambdaDeserializerRef def srBoxesRunTimeRef : ClassBType = _coreBTypes.srBoxesRunTimeRef + def srBoxedUnitRef : ClassBType = _coreBTypes.srBoxedUnitRef def hashMethodSym: Symbol = _coreBTypes.hashMethodSym diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala index 204c6a33ce..e4fdcdc542 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala @@ -7,6 +7,7 @@ import scala.tools.asm.{Opcodes, Handle, Type, Label} import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis.{Frame, BasicInterpreter, Analyzer, Value} import scala.tools.nsc.backend.jvm.BTypes._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ import java.lang.invoke.LambdaMetafactory import scala.collection.mutable @@ -23,6 +24,7 @@ import scala.collection.convert.decorateAsScala._ */ class BackendUtils[BT <: BTypes](val btypes: BT) { import btypes._ + import callGraph.ClosureInstantiation /** * A wrapper to make ASM's Analyzer a bit easier to use. @@ -141,6 +143,14 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { (result, map, hasSerializableClosureInstantiation) } + def getBoxedUnit: FieldInsnNode = new FieldInsnNode(Opcodes.GETSTATIC, coreBTypes.srBoxedUnitRef.internalName, "UNIT", coreBTypes.srBoxedUnitRef.descriptor) + + private val anonfunAdaptedName = """.*\$anonfun\$\d+\$adapted""" + def hasAdaptedImplMethod(closureInit: ClosureInstantiation): Boolean = { + BytecodeUtils.isrJFunctionType(Type.getReturnType(closureInit.lambdaMetaFactoryCall.indy.desc).getInternalName) && + closureInit.lambdaMetaFactoryCall.implMethod.getName.matches(anonfunAdaptedName) + } + /** * Visit the class node and collect all referenced nested classes. */ diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index b192e1b46a..64677ddcc0 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -461,7 +461,7 @@ class CallGraph[BT <: BTypes](val btypes: BT) { // When re-writing the closure callsite to the implMethod, we have to insert a cast. // // The check below ensures that - // (1) the implMethod type has the expected singature (captured types plus argument types + // (1) the implMethod type has the expected signature (captured types plus argument types // from instantiatedMethodType) // (2) the receiver of the implMethod matches the first captured type // (3) all parameters that are not the same in samMethodType and instantiatedMethodType diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala index 4203a93f2e..4e1e878aa8 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala @@ -11,7 +11,7 @@ import scala.annotation.switch import scala.collection.immutable import scala.collection.immutable.IntMap import scala.reflect.internal.util.NoPosition -import scala.tools.asm.{Type, Opcodes} +import scala.tools.asm.{Handle, Type, Opcodes} import scala.tools.asm.tree._ import scala.tools.nsc.backend.jvm.BTypes.InternalName import BytecodeUtils._ @@ -23,7 +23,9 @@ import scala.collection.convert.decorateAsScala._ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { import btypes._ import callGraph._ + import coreBTypes._ import backendUtils._ + import ClosureOptimizer._ /** * If a closure is allocated and invoked within the same method, re-write the invocation to the @@ -81,6 +83,7 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { // We don't need to worry about the method being too large for running an analysis: large // methods are not added to the call graph / closureInstantiations map. lazy val prodCons = new ProdConsAnalyzer(methodNode, closureInits.valuesIterator.next().ownerClass.internalName) + // sorting for bytecode stability (e.g. indices of local vars created during the rewrite) val sortedInits = immutable.TreeSet.empty ++ closureInits.values sortedInits.iterator.map(init => (init, closureCallsites(init, prodCons))).filter(_._2.nonEmpty) }).toList // mapping to a list (not a map) to keep the sorting @@ -118,20 +121,7 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { val argTypes = closureInit.lambdaMetaFactoryCall.samMethodType.getArgumentTypes val firstArgLocal = ownerMethod.maxLocals - // The comment in the unapply method of `LambdaMetaFactoryCall` explains why we have to introduce - // casts for arguments that have different types in samMethodType and instantiatedMethodType. - val castLoadTypes = { - val instantiatedMethodType = closureInit.lambdaMetaFactoryCall.instantiatedMethodType - (argTypes, instantiatedMethodType.getArgumentTypes).zipped map { - case (samArgType, instantiatedArgType) if samArgType != instantiatedArgType => - // the LambdaMetaFactoryCall extractor ensures that the two types are reference types, - // so we don't end up casting primitive values. - Some(instantiatedArgType) - case _ => - None - } - } - val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes, castLoadTypes) + val argLocals = LocalsList.fromTypes(firstArgLocal, argTypes) ownerMethod.maxLocals = firstArgLocal + argLocals.size (captureLocals, argLocals) @@ -169,6 +159,28 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { }).toList } + /** + * Check whether `invocation` invokes the SAM of the IndyLambda `closureInit`. + * + * In addition to a perfect match, we also identify cases where a generic FunctionN is created + * but the invocation is to a specialized variant apply$sp... Vice-versa, we also allow the + * case where a specialized FunctionN$sp.. is created but the generic apply is invoked. In + * these cases, the translation will introduce the necessary box / unbox invocations. Example: + * + * val f: Int => Any = (x: Int) => 1 + * f(10) + * + * The IndyLambda creates a specialized `JFunction1$mcII$sp`, whose SAM is `apply$mcII$sp(I)I`. + * The invocation calls `apply(Object)Object`: the method name and type don't match. + * We identify these cases, insert the necessary unbox operation for the arguments, and invoke + * the `$anonfun(I)I` method. + * + * Tests in InlinerTest.optimizeSpecializedClosures. In that test, methods t4/t4a/t5/t8 show + * examples where the parameters have to be unboxed because generic `apply` is called, but the + * lambda body method takes primitive types. + * The opposite case is in t9: a the specialized `apply$sp..` is invoked, but the lambda body + * method takes boxed arguments, so we have to insert boxing operations. + */ private def isSamInvocation(invocation: MethodInsnNode, closureInit: ClosureInstantiation, prodCons: => ProdConsAnalyzer): Boolean = { val indy = closureInit.lambdaMetaFactoryCall.indy if (invocation.getOpcode == INVOKESTATIC) false @@ -183,11 +195,97 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { receiverProducers.size == 1 && receiverProducers.head == indy } - invocation.name == indy.name && { - val indySamMethodDesc = closureInit.lambdaMetaFactoryCall.samMethodType.getDescriptor - indySamMethodDesc == invocation.desc - } && - closureIsReceiver // most expensive check last + def isSpecializedVersion(specName: String, nonSpecName: String) = specName.startsWith(nonSpecName) && specName.substring(nonSpecName.length).matches(specializationSuffix) + + def sameOrSpecializedType(specTp: Type, nonSpecTp: Type) = { + specTp == nonSpecTp || { + val specDesc = specTp.getDescriptor + val nonSpecDesc = nonSpecTp.getDescriptor + specDesc.length == 1 && primitives.contains(specDesc) && nonSpecDesc == ObjectRef.descriptor + } + } + + def specializedDescMatches(specMethodDesc: String, nonSpecMethodDesc: String) = { + val specArgs = Type.getArgumentTypes(specMethodDesc) + val nonSpecArgs = Type.getArgumentTypes(nonSpecMethodDesc) + specArgs.corresponds(nonSpecArgs)(sameOrSpecializedType) && sameOrSpecializedType(Type.getReturnType(specMethodDesc), Type.getReturnType(nonSpecMethodDesc)) + } + + def nameAndDescMatch = { + val aName = invocation.name + val bName = indy.name + val aDesc = invocation.desc + val bDesc = closureInit.lambdaMetaFactoryCall.samMethodType.getDescriptor + if (aName == bName) aDesc == bDesc + else if (isSpecializedVersion(aName, bName)) specializedDescMatches(aDesc, bDesc) + else if (isSpecializedVersion(bName, aName)) specializedDescMatches(bDesc, aDesc) + else false + } + + nameAndDescMatch && closureIsReceiver // most expensive check last + } + } + + private def isPrimitiveType(asmType: Type) = { + val sort = asmType.getSort + Type.VOID <= sort && sort <= Type.DOUBLE + } + + private def unboxOp(primitiveType: Type): MethodInsnNode = { + val bType = bTypeForDescriptorOrInternalNameFromClassfile(primitiveType.getDescriptor) + val MethodNameAndType(name, methodBType) = asmUnboxTo(bType) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + private def boxOp(primitiveType: Type): MethodInsnNode = { + val bType = bTypeForDescriptorOrInternalNameFromClassfile(primitiveType.getDescriptor) + val MethodNameAndType(name, methodBType) = asmBoxTo(bType) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + /** + * The argument types of the lambda body method may differ in two ways from the argument types of + * the closure member method that is invoked (and replaced by a call to the body). + * - The lambda body method may have more specific types than the invoked closure member, see + * comment in [[LambdaMetaFactoryCall.unapply]]. + * - The invoked closure member might be a specialized variant of the SAM or vice-versa, see + * comment method [[isSamInvocation]]. + */ + private def adaptStoredArguments(closureInit: ClosureInstantiation, invocation: MethodInsnNode): Int => Option[AbstractInsnNode] = { + val invokeDesc = invocation.desc + // The lambda body method has additional parameters for captured values. Here we need to consider + // only those parameters of the body method that correspond to lambda parameters. This happens + // to be exactly LMF.instantiatedMethodType. In fact, `LambdaMetaFactoryCall.unapply` ensures + // that the body method signature is exactly (capturedParams + instantiatedMethodType). + val lambdaBodyMethodDescWithoutCaptures = closureInit.lambdaMetaFactoryCall.instantiatedMethodType.getDescriptor + if (invokeDesc == lambdaBodyMethodDescWithoutCaptures) { + _ => None + } else { + val invokeArgTypes = Type.getArgumentTypes(invokeDesc) + val implMethodArgTypes = Type.getArgumentTypes(lambdaBodyMethodDescWithoutCaptures) + val res = new Array[Option[AbstractInsnNode]](invokeArgTypes.length) + for (i <- invokeArgTypes.indices) { + if (invokeArgTypes(i) == implMethodArgTypes(i)) { + res(i) = None + } else if (isPrimitiveType(implMethodArgTypes(i)) && invokeArgTypes(i).getDescriptor == ObjectRef.descriptor) { + res(i) = Some(unboxOp(implMethodArgTypes(i))) + } else if (isPrimitiveType(invokeArgTypes(i)) && implMethodArgTypes(i).getDescriptor == ObjectRef.descriptor) { + res(i) = Some(boxOp(invokeArgTypes(i))) + } else { + assert(!isPrimitiveType(invokeArgTypes(i)), invokeArgTypes(i)) + assert(!isPrimitiveType(implMethodArgTypes(i)), implMethodArgTypes(i)) + // The comment in the unapply method of `LambdaMetaFactoryCall` explains why we have to introduce + // casts for arguments that have different types in samMethodType and instantiatedMethodType. + // + // Note: + // - invokeArgTypes is the same as the argument types in the IndyLambda's samMethodType, + // this is ensured by the `isSamInvocation` filter in this file + // - implMethodArgTypes is the same as the arg types in the IndyLambda's instantiatedMethodType, + // this is ensured by the unapply method in LambdaMetaFactoryCall (file CallGraph) + res(i) = Some(new TypeInsnNode(CHECKCAST, implMethodArgTypes(i).getInternalName)) + } + } + res } } @@ -196,7 +294,7 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { val lambdaBodyHandle = closureInit.lambdaMetaFactoryCall.implMethod // store arguments - insertStoreOps(invocation, ownerMethod, argumentLocalsList) + insertStoreOps(invocation, ownerMethod, argumentLocalsList, adaptStoredArguments(closureInit, invocation)) // drop the closure from the stack ownerMethod.instructions.insertBefore(invocation, new InsnNode(POP)) @@ -228,8 +326,22 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { val bodyInvocation = new MethodInsnNode(bodyOpcode, lambdaBodyHandle.getOwner, lambdaBodyHandle.getName, lambdaBodyHandle.getDesc, isInterface) ownerMethod.instructions.insertBefore(invocation, bodyInvocation) - val returnType = Type.getReturnType(lambdaBodyHandle.getDesc) - fixLoadedNothingOrNullValue(returnType, bodyInvocation, ownerMethod, btypes) // see comment of that method + val bodyReturnType = Type.getReturnType(lambdaBodyHandle.getDesc) + val invocationReturnType = Type.getReturnType(invocation.desc) + if (isPrimitiveType(invocationReturnType) && bodyReturnType.getDescriptor == ObjectRef.descriptor) { + val op = + if (invocationReturnType.getSort == Type.VOID) getPop(1) + else unboxOp(invocationReturnType) + ownerMethod.instructions.insertBefore(invocation, op) + } else if (isPrimitiveType(bodyReturnType) && invocationReturnType.getDescriptor == ObjectRef.descriptor) { + val op = + if (bodyReturnType.getSort == Type.VOID) getBoxedUnit + else boxOp(bodyReturnType) + ownerMethod.instructions.insertBefore(invocation, op) + } else { + // see comment of that method + fixLoadedNothingOrNullValue(bodyReturnType, bodyInvocation, ownerMethod, btypes) + } ownerMethod.instructions.remove(invocation) @@ -277,6 +389,9 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { // Rewriting a closure invocation may render code unreachable. For example, the body method of // (x: T) => ??? has return type Nothing$, and an ATHROW is added (see fixLoadedNothingOrNullValue). unreachableCodeEliminated -= ownerMethod + + if (hasAdaptedImplMethod(closureInit) && inliner.canInlineBody(bodyMethodCallsite).isEmpty) + inliner.inlineCallsite(bodyMethodCallsite) } /** @@ -293,13 +408,10 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { // local. On the other hand, further optimizations (copy propagation, remove unused locals) will // clean it up. - // Captured variables don't need to be cast when loaded at the callsite (castLoadTypes are None). - // This is checked in `isClosureInstantiation`: the types of the captured variables in the indy - // instruction match exactly the corresponding parameter types in the body method. - val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes, castLoadTypes = _ => None) + val localsForCaptures = LocalsList.fromTypes(firstCaptureLocal, capturedTypes) closureInit.ownerMethod.maxLocals = firstCaptureLocal + localsForCaptures.size - insertStoreOps(indy, closureInit.ownerMethod, localsForCaptures) + insertStoreOps(indy, closureInit.ownerMethod, localsForCaptures, _ => None) insertLoadOps(indy, closureInit.ownerMethod, localsForCaptures) localsForCaptures @@ -311,8 +423,16 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * * The lowest stack value is stored in the head of the locals list, so the last local is stored first. */ - private def insertStoreOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList) = - insertLocalValueOps(before, methodNode, localsList, store = true) + private def insertStoreOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList, beforeStore: Int => Option[AbstractInsnNode]) = { + // The first instruction needs to store into the last local of the `localsList`. + // To avoid reversing the list, we use `insert(previous)`. + val previous = before.getPrevious + def ins(op: AbstractInsnNode) = methodNode.instructions.insert(previous, op) + for ((l, i) <- localsList.locals.zipWithIndex) { + ins(new VarInsnNode(l.storeOpcode, l.local)) + beforeStore(i) foreach ins + } + } /** * Insert load operations in front of the `before` instruction to copy the local values denoted @@ -320,20 +440,10 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * * The head of the locals list will be the lowest value on the stack, so the first local is loaded first. */ - private def insertLoadOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList) = - insertLocalValueOps(before, methodNode, localsList, store = false) - - private def insertLocalValueOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList, store: Boolean): Unit = { - // If `store` is true, the first instruction needs to store into the last local of the `localsList`. - // Load instructions on the other hand are emitted in the order of the list. - // To avoid reversing the list, we use `insert(previousInstr)` for stores and `insertBefore(before)` for loads. - lazy val previous = before.getPrevious + private def insertLoadOps(before: AbstractInsnNode, methodNode: MethodNode, localsList: LocalsList) = { for (l <- localsList.locals) { - val varOp = new VarInsnNode(if (store) l.storeOpcode else l.loadOpcode, l.local) - if (store) methodNode.instructions.insert(previous, varOp) - else methodNode.instructions.insertBefore(before, varOp) - if (!store) for (castType <- l.castLoadedValue) - methodNode.instructions.insert(varOp, new TypeInsnNode(CHECKCAST, castType.getInternalName)) + val op = new VarInsnNode(l.loadOpcode, l.local) + methodNode.instructions.insertBefore(before, op) } } @@ -355,12 +465,12 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * Local(6, refOpOffset) :: * Nil */ - def fromTypes(firstLocal: Int, types: Array[Type], castLoadTypes: Int => Option[Type]): LocalsList = { + def fromTypes(firstLocal: Int, types: Array[Type]): LocalsList = { var sizeTwoOffset = 0 val locals: List[Local] = types.indices.map(i => { // The ASM method `type.getOpcode` returns the opcode for operating on a value of `type`. val offset = types(i).getOpcode(ILOAD) - ILOAD - val local = Local(firstLocal + i + sizeTwoOffset, offset, castLoadTypes(i)) + val local = Local(firstLocal + i + sizeTwoOffset, offset) if (local.size == 2) sizeTwoOffset += 1 local })(collection.breakOut) @@ -374,10 +484,15 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { * The xLOAD / xSTORE opcodes are in the following sequence: I, L, F, D, A, so the offset for * a local variable holding a reference (`A`) is 4. See also method `getOpcode` in [[scala.tools.asm.Type]]. */ - case class Local(local: Int, opcodeOffset: Int, castLoadedValue: Option[Type]) { + case class Local(local: Int, opcodeOffset: Int) { def size = if (loadOpcode == LLOAD || loadOpcode == DLOAD) 2 else 1 def loadOpcode = ILOAD + opcodeOffset def storeOpcode = ISTORE + opcodeOffset } } + +object ClosureOptimizer { + val primitives = "BSIJCFDZV" + val specializationSuffix = s"(\\$$mc[$primitives]+\\$$sp)" +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala index 54724458e2..99041b5497 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala @@ -70,4 +70,21 @@ class ClosureOptimizerTest extends ClearAfterClass { assert(bodyCall.getNext.getOpcode == POP) assert(bodyCall.getNext.getNext.getOpcode == ACONST_NULL) } + + @Test + def makeLMFCastExplicit(): Unit = { + val code = + """class C { + | def t(l: List[String]) = { + | val fun: String => String = s => s + | fun(l.head) + | } + |} + """.stripMargin + val List(c) = compileClasses(compiler)(code) + assertSameCode(getSingleMethod(c, "t").instructions.dropNonOp, + List(VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/collection/immutable/List", "head", "()Ljava/lang/Object;", false), + TypeOp(CHECKCAST, "java/lang/String"), Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(Ljava/lang/String;)Ljava/lang/String;", false), + TypeOp(CHECKCAST, "java/lang/String"), Op(ARETURN))) + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 1037fd5221..75c1a0d3ad 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -1226,4 +1226,194 @@ class InlinerTest extends ClearAfterClass { assertNoInvoke(getSingleMethod(c, "g")) assertNoInvoke(getSingleMethod(d, "t")) } + + @Test + def optimizeSpecializedClosures(): Unit = { + val code = + """class ValKl(val x: Int) extends AnyVal + | + |class C { + | def t1 = { + | // IndyLambda: SAM type is JFunction1$mcII$sp, SAM is apply$mcII$sp(I)I, body method is $anonfun(I)I + | val f = (x: Int) => x + 1 + | // invocation of apply$mcII$sp(I)I, matches the SAM in IndyLambda. no boxing / unboxing needed. + | f(10) + | // opt: re-write the invocation to the body method + | } + | + | @inline final def m1a(f: Long => Int) = f(1l) + | def t1a = m1a(l => l.toInt) // after inlining m1a, we have the same situation as in t1 + | + | def t2 = { + | // there is no specialized variant of Function2 for this combination of types, so the IndyLambda has to create a generic Function2. + | // IndyLambda: SAM type is JFunction2, SAM is apply(ObjectObject)Object, body method is $anonfun$adapted(ObjectObject)Object + | val f = (b: Byte, i: Int) => i + b + | // invocation of apply(ObjectOjbect)Object, matches SAM in IndyLambda. arguments are boxed, result unboxed. + | f(1, 2) + | // opt: re-wrtie to $anonfun$adapted + | // inline that call, then we get box-unbox pairs (can be eliminated) and a call to $anonfun(BI)I + | } + | + | def t3 = { + | // similar to t2: for functions with value class parameters, IndyLambda always uses the generic Function version. + | // IndyLambda: SAM type is JFunction1, SAM is apply(Object)Object, body method is $anonfun$adapted(Object)Object + | val f = (a: ValKl) => a + | // invocation of apply(Object)Object, ValKl instance is created, result extracted + | f(new ValKl(1)) + | // opt: re-write to $anonfun$adapted. + | // inline that call, then we get value class instantiation-extraction pairs and a call to $anonfun(I)I + | } + | + | def t4 = { + | // IndyLambda: SAM type is JFunction1$mcII$sp, SAM is apply$mcII$sp(I)I, body method is $anonfun(I)I + | val f: Int => Any = (x: Int) => 1 + | // invocation of apply(Object)Object, argument is boxed. method name and type doesn't match IndyLambda. + | f(10) + | // opt: rewriting to the body method requires inserting an unbox operation for the argument, and a box operation for the result + | // that produces a box-unbox pair and a call to $anonfun(I)I + | } + | + | + | @inline final def m4a[T, U, V](f: (T, U) => V, x: T, y: U) = f(x, y) // invocation to generic apply(ObjectObject)Object + | def t4a = m4a((x: Int, y: Double) => 1l + x + y.toLong, 1, 2d) // IndyLambda uses specilized JFunction2$mcJID$sp. after inlining m4a, similar to t4. + | + | def t5 = { + | // no specialization for the comibnation of primitives + | // IndyLambda: SAM type is JFunction2, SAM is generic apply, body method is $anonfun$adapted + | val f: (Int, Byte) => Any = (x: Int, b: Byte) => 1 + | // invocation of generic apply. + | f(10, 3) + | // opt: re-write to $anonfun$adapted, inline that method. generates box-unbox pairs and a call to $anonfun(IB)I + | } + | + | def t5a = m4a((x: Int, y: Byte) => 1, 12, 31.toByte) // similar to t5 after inlining m4a + | + | // m6$mIVc$sp invokes apply$mcVI$sp + | @inline final def m6[@specialized(Int) T, @specialized(Unit) U](f: T => U, x: T): Unit = f(x) + | // IndyLambda: JFunction1$mcVI$sp, SAM is apply$mcVI$sp, body method $anonfun(I)V + | // invokes m6$mIVc$sp (Lscala/Function1;I)V + | def t6 = m6((x: Int) => (), 10) + | // opt: after inlining m6, the closure method invocation (apply$mcVI$sp) matches the IndyLambda, the call can be rewritten, no boxing + | + | // m7 invokes apply + | @inline final def m7[@specialized(Boolean) T, @specialized(Int) U](f: T => U, x: T): Unit = f(x) + | // IndyLambda: JFunction1, SAM is apply(Object)Object, body method is $anonfun$adapted(Obj)Obj + | // `true` is boxed before passing to m7 + | def t7 = m7((x: Boolean) => (), true) + | // opt: after inlining m7, the apply call is re-written to $anonfun$adapted, which is then inlined. + | // we get a box-unbox pair and a call to $anonfun(Z)V + | + | + | // invokes the generic apply(ObjObj)Obj + | @inline final def m8[T, U, V](f: (T, U) => V, x: T, y: U) = f(x, y) + | // IndyLambda: JFunction2$mcJID$sp, SAM is apply$mcJID$sp, body method $anonfun(ID)J + | // boxes the int and double arguments and calls m8, unboxToLong the result + | def t8 = m8((x: Int, y: Double) => 1l + x + y.toLong, 1, 2d) + | // opt: after inlining m8, rewrite to the body method $anonfun(ID)J, which requires inserting unbox operations for the params, box for the result + | // the box-unbox pairs can then be optimized away + | + | // m9$mVc$sp invokes apply$mcVI$sp + | @inline final def m9[@specialized(Unit) U](f: Int => U): Unit = f(1) + | // IndyLambda: JFunction1, SAM is apply(Obj)Obj, body method $anonfun$adapted(Ojb)Obj + | // invocation of m9$mVc$sp + | def t9 = m9(println) + | // opt: after inlining m9, rewrite to $anonfun$adapted(Ojb)Obj, which requires inserting a box operation for the parameter. + | // then we inline $adapted, which has signature (Obj)V. the `BoxedUnit.UNIT` from the body of $anonfun$adapted is eliminated by push-pop + | + | def t9a = (1 to 10) foreach println // similar to t9 + | + | def intCons(i: Int): Unit = () + | // IndyLambda: JFunction1$mcVI$sp, SAM is apply$mcVI$sp, body method $anonfun(I)V + | def t10 = m9(intCons) + | // after inlining m9, rewrite the apply$mcVI$sp call to the body method, no adaptations required + | + | def t10a = (1 to 10) foreach intCons // similar to t10 + |} + """.stripMargin + val List(c, _, _) = compile(code) + + def instructionSummary(m: Method): List[Any] = m.instructions.dropNonOp map { + case i: Invoke => i.name + case i => i.opcode + } + + assertEquals(instructionSummary(getSingleMethod(c, "t1")), + List(BIPUSH, "C$$$anonfun$1", IRETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t1a")), + List(LCONST_1, "C$$$anonfun$2", IRETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t2")), List( + ICONST_1, "boxToByte", + ICONST_2, "boxToInteger", ASTORE, + "unboxToByte", + ALOAD, "unboxToInt", + "C$$$anonfun$3", + "boxToInteger", "unboxToInt", IRETURN)) + + // val a = new ValKl(n); new ValKl(anonfun(a.x)).x + // value class instantiation-extraction should be optimized by boxing elim + assertEquals(instructionSummary(getSingleMethod(c, "t3")), List( + NEW, DUP, ICONST_1, "", ASTORE, + NEW, DUP, ALOAD, CHECKCAST, "x", + "C$$$anonfun$4", + "", CHECKCAST, + "x", IRETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t4")), List( + BIPUSH, "boxToInteger", "unboxToInt", + "C$$$anonfun$5", + "boxToInteger", ARETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t4a")), List( + ICONST_1, "boxToInteger", + LDC, "boxToDouble", "unboxToDouble", DSTORE, + "unboxToInt", DLOAD, "C$$$anonfun$6", + "boxToLong", "unboxToLong", LRETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t5")), List( + BIPUSH, "boxToInteger", + ICONST_3, "boxToByte", ASTORE, + "unboxToInt", ALOAD, + "unboxToByte", + "C$$$anonfun$7", + "boxToInteger", ARETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t5a")), List( + BIPUSH, "boxToInteger", + BIPUSH, I2B, "boxToByte", ASTORE, + "unboxToInt", ALOAD, + "unboxToByte", + "C$$$anonfun$8", + "boxToInteger", "unboxToInt", IRETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t6")), List( + BIPUSH, "C$$$anonfun$9", RETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t7")), List( + ICONST_1, "boxToBoolean", "unboxToBoolean", + "C$$$anonfun$10", RETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t8")), List( + ICONST_1, "boxToInteger", + LDC, "boxToDouble", "unboxToDouble", DSTORE, + "unboxToInt", DLOAD, "C$$$anonfun$11", + "boxToLong", "unboxToLong", LRETURN)) + + assertEquals(instructionSummary(getSingleMethod(c, "t9")), List( + ICONST_1, "boxToInteger", "C$$$anonfun$12", RETURN)) + + // t9a inlines Range.foreach, which is quite a bit of code, so just testing the core + assertInvoke(getSingleMethod(c, "t9a"), "C", "C$$$anonfun$13") + assert(instructionSummary(getSingleMethod(c, "t9a")).contains("boxToInteger")) + + assertEquals(instructionSummary(getSingleMethod(c, "t10")), List( + ICONST_1, ISTORE, + ALOAD, ILOAD, + "C$$$anonfun$14", RETURN)) + + // t10a inlines Range.foreach + assertInvoke(getSingleMethod(c, "t10a"), "C", "C$$$anonfun$15") + assert(!instructionSummary(getSingleMethod(c, "t10a")).contains("boxToInteger")) + } } -- cgit v1.2.3 From 11614455e71530292c1ed98dc4fbda5efee0b84d Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Mon, 23 Nov 2015 15:26:45 +0100 Subject: Fix in "consumersOfOutputsFrom" query in prod-cons analysis Fix the "consumersOfOutputsFrom" in the prod-cons analysis to support dummy instructions: - UninitializedLocalProducer - ParameterProducer - ExceptionProducer --- .../jvm/analysis/ProdConsAnalyzerImpl.scala | 31 +++++++++++++++------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala index f7ec55f305..894799bf36 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/ProdConsAnalyzerImpl.scala @@ -94,8 +94,13 @@ trait ProdConsAnalyzerImpl { inputValues(insn).iterator.flatMap(v => v.insns.asScala).toSet } - def consumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = - _consumersOfOutputsFrom.get(insn).map(v => v.indices.flatMap(v.apply)(collection.breakOut): Set[AbstractInsnNode]).getOrElse(Set.empty) + def consumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = insn match { + case _: UninitializedLocalProducer => Set.empty + case ParameterProducer(local) => consumersOfValueAt(methodNode.instructions.getFirst, local) + case ExceptionProducer(handlerLabel, handlerFrame) => consumersOfValueAt(handlerLabel, handlerFrame.stackTop) + case _ => + _consumersOfOutputsFrom.get(insn).map(v => v.indices.flatMap(v.apply)(collection.breakOut): Set[AbstractInsnNode]).getOrElse(Set.empty) + } /** * Returns the potential initial producer instructions of a value in the frame of `insn`. @@ -151,9 +156,15 @@ trait ProdConsAnalyzerImpl { inputValueSlots(insn).flatMap(slot => initialProducersForValueAt(insn, slot)).toSet } - def ultimateConsumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = { - lazy val next = insn.getNext - outputValueSlots(insn).flatMap(slot => ultimateConsumersOfValueAt(next, slot)).toSet + def ultimateConsumersOfOutputsFrom(insn: AbstractInsnNode): Set[AbstractInsnNode] = insn match { + case _: UninitializedLocalProducer => Set.empty + case _ => + lazy val next = insn match { + case _: ParameterProducer => methodNode.instructions.getFirst + case ExceptionProducer(handlerLabel, _) => handlerLabel + case _ => insn.getNext + } + outputValueSlots(insn).flatMap(slot => ultimateConsumersOfValueAt(next, slot)).toSet } private def isCopyOperation(insn: AbstractInsnNode): Boolean = { @@ -378,7 +389,7 @@ trait ProdConsAnalyzerImpl { private def outputValueSlots(insn: AbstractInsnNode): Seq[Int] = insn match { case ParameterProducer(local) => Seq(local) case UninitializedLocalProducer(local) => Seq(local) - case ExceptionProducer(frame) => Seq(frame.stackTop) + case ExceptionProducer(_, frame) => Seq(frame.stackTop) case _ => if (insn.getOpcode == -1) return Seq.empty if (isStore(insn)) { @@ -443,9 +454,9 @@ abstract class InitialProducer extends AbstractInsnNode(-1) { override def accept(cv: MethodVisitor): Unit = throw new UnsupportedOperationException } -case class ParameterProducer(local: Int) extends InitialProducer -case class UninitializedLocalProducer(local: Int) extends InitialProducer -case class ExceptionProducer[V <: Value](handlerFrame: Frame[V]) extends InitialProducer +case class ParameterProducer(local: Int) extends InitialProducer +case class UninitializedLocalProducer(local: Int) extends InitialProducer +case class ExceptionProducer[V <: Value](handlerLabel: LabelNode, handlerFrame: Frame[V]) extends InitialProducer class InitialProducerSourceInterpreter extends SourceInterpreter { override def newParameterValue(isInstanceMethod: Boolean, local: Int, tp: Type): SourceValue = { @@ -457,6 +468,6 @@ class InitialProducerSourceInterpreter extends SourceInterpreter { } override def newExceptionValue(tryCatchBlockNode: TryCatchBlockNode, handlerFrame: Frame[_ <: Value], exceptionType: Type): SourceValue = { - new SourceValue(1, ExceptionProducer(handlerFrame)) + new SourceValue(1, ExceptionProducer(tryCatchBlockNode.handler, handlerFrame)) } } -- cgit v1.2.3 From ecbca547ac04ad33d336c0d41e72193a17dcf0ab Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Sun, 13 Dec 2015 09:39:43 +0100 Subject: Don't run closure optimizer on methods too large for prod-cons For methods that are too large to run a producers-consumers analysis, don't run the closure optimizer. This is an oversight, all data flow analyses in the backend are guarded by a method size check. --- .../tools/nsc/backend/jvm/opt/ClosureOptimizer.scala | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala index 4e1e878aa8..61d10eeae9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala @@ -79,13 +79,15 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { val callsitesToRewrite: List[(ClosureInstantiation, List[Either[RewriteClosureApplyToClosureBodyFailed, (MethodInsnNode, Int)]])] = { closureInstantiations.iterator.flatMap({ case (methodNode, closureInits) => - // A lazy val to ensure the analysis only runs if necessary (the value is passed by name to `closureCallsites`) - // We don't need to worry about the method being too large for running an analysis: large - // methods are not added to the call graph / closureInstantiations map. - lazy val prodCons = new ProdConsAnalyzer(methodNode, closureInits.valuesIterator.next().ownerClass.internalName) - // sorting for bytecode stability (e.g. indices of local vars created during the rewrite) - val sortedInits = immutable.TreeSet.empty ++ closureInits.values - sortedInits.iterator.map(init => (init, closureCallsites(init, prodCons))).filter(_._2.nonEmpty) + if (!AsmAnalyzer.sizeOKForSourceValue(methodNode)) Nil else { + // A lazy val to ensure the analysis only runs if necessary (the value is passed by name to `closureCallsites`) + // We don't need to worry about the method being too large for running an analysis: large + // methods are not added to the call graph / closureInstantiations map. + lazy val prodCons = new ProdConsAnalyzer(methodNode, closureInits.valuesIterator.next().ownerClass.internalName) + // sorting for bytecode stability (e.g. indices of local vars created during the rewrite) + val sortedInits = immutable.TreeSet.empty ++ closureInits.values + sortedInits.iterator.map(init => (init, closureCallsites(init, prodCons))).filter(_._2.nonEmpty) + } }).toList // mapping to a list (not a map) to keep the sorting } -- cgit v1.2.3 From b1fd1e83ab6ab3bf71b1a9c22794e8fd0c416d0f Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Sat, 12 Dec 2015 20:22:26 +0100 Subject: Fix push-pop elimination for values pushed by DUP If a DUP is consumed by two POPs, ensure that the DUP and its producer are eliminated. Before, only the DUP was eliminated, leaving an unused value on the stack. --- .../scala/tools/nsc/backend/jvm/AsmUtils.scala | 4 +- .../jvm/analysis/InstructionStackEffect.scala | 8 +-- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 74 +++++++++++++--------- 3 files changed, 49 insertions(+), 37 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala index 18a495e5fd..a921ef3966 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/AsmUtils.scala @@ -122,12 +122,12 @@ object AsmUtils { * Run ASM's CheckClassAdapter over a class. Returns None if no problem is found, otherwise * Some(msg) with the verifier's error message. */ - def checkClass(classNode: ClassNode): Option[String] = { + def checkClass(classNode: ClassNode, dumpNonErroneous: Boolean = false): Option[String] = { val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) classNode.accept(cw) val sw = new StringWriter() val pw = new PrintWriter(sw) - CheckClassAdapter.verify(new ClassReader(cw.toByteArray), false, pw) + CheckClassAdapter.verify(new ClassReader(cw.toByteArray), dumpNonErroneous, pw) val res = sw.toString if (res.isEmpty) None else Some(res) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala index 4e81018451..2181f00850 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala @@ -27,7 +27,7 @@ object InstructionStackEffect { * This method requires the `frame` to be in the state **before** executing / interpreting the * `insn`. */ - def forAsmAnalysis[V <: Value](insn: AbstractInsnNode, frame: Frame[V]): Int = computeConsProd(insn, frame = frame) + def forAsmAnalysis[V <: Value](insn: AbstractInsnNode, frame: Frame[V]): Int = computeConsProd(insn, forClassfile = false, conservative = false, frame = frame) /** * Returns the maximal possible growth of the stack when executing `insn`. The returned value @@ -41,7 +41,7 @@ object InstructionStackEffect { * allows looking up the sizes of values on the stack. */ def maxStackGrowth(insn: AbstractInsnNode): Int = { - val prodCons = computeConsProd(insn, conservative = true) + val prodCons = computeConsProd(insn, forClassfile = false, conservative = true) prod(prodCons) - cons(prodCons) } @@ -50,7 +50,7 @@ object InstructionStackEffect { * (the `cons` / `prod` extract individual values). The returned values are correct for writing * into a classfile (see doc on the `analysis` package object). */ - def forClassfile(insn: AbstractInsnNode): Int = computeConsProd(insn, forClassfile = true) + def forClassfile(insn: AbstractInsnNode): Int = computeConsProd(insn, forClassfile = true, conservative = false) private def invokeConsProd(methodDesc: String, insn: AbstractInsnNode, forClassfile: Boolean): Int = { val consumesReceiver = insn.getOpcode != INVOKESTATIC && insn.getOpcode != INVOKEDYNAMIC @@ -71,7 +71,7 @@ object InstructionStackEffect { d == "J" || d == "D" } - private def computeConsProd[V <: Value](insn: AbstractInsnNode, forClassfile: Boolean = false, conservative: Boolean = false, frame: Frame[V] = null): Int = { + private def computeConsProd[V <: Value](insn: AbstractInsnNode, forClassfile: Boolean, conservative: Boolean, frame: Frame[V] = null): Int = { // not used if `forClassfile || conservative`: in these cases, `frame` is allowed to be `null` def peekStack(n: Int): V = frame.peekStack(n) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index b4868a03ef..af0223b449 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -480,41 +480,48 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { lazy val prodCons = new ProdConsAnalyzer(method, owner) - /** - * True if the values produced by `prod` are all the same. Most instructions produce a single - * value. DUP and DUP2 (with a size-2 input) produce two equivalent values. However, there - * are some exotic instructions that produce multiple non-equal values (DUP_X1, SWAP, ...). - * - * Assume we have `DUP_X2; POP`. In order to remove the `POP` we need to change the DUP_X2 - * into something else, which is not straightforward. - * - * Since scalac never emits any of those exotic bytecodes, we don't optimize them. - */ - def producerHasSingleOutput(prod: AbstractInsnNode): Boolean = (prod.getOpcode: @switch) match { - case DUP_X1 | DUP_X2 | DUP2_X1 | DUP2_X2 | SWAP => false - case DUP2 => prodCons.frameAt(prod).peekStack(0).getSize == 2 - case _ => true - } - - // Set.empty if not a single consumer, or if the producer is the exception value of a catch block /** * Returns the producers for the stack value `inputSlot` consumed by `cons`, if the consumer * instruction is the only consumer for all of these producers. * - * I a producer has multiple consumers, or the value is the caught exception in a catch + * If a producer has multiple consumers, or the value is the caught exception in a catch * block, this method returns Set.empty. */ def producersIfSingleConsumer(cons: AbstractInsnNode, inputSlot: Int): Set[AbstractInsnNode] = { + /** + * True if the values produced by `prod` are all the same. Most instructions produce a single + * value. DUP and DUP2 (with a size-2 input) produce two equivalent values. However, there + * are some exotic instructions that produce multiple non-equal values (DUP_X1, SWAP, ...). + * + * Assume we have `DUP_X2; POP`. In order to remove the `POP` we need to change the DUP_X2 + * into something else, which is not straightforward. + * + * Since scalac never emits any of those exotic bytecodes, we don't optimize them. + */ + def producerHasSingleOutput(prod: AbstractInsnNode): Boolean = prod match { + case _: ExceptionProducer[_] | _: UninitializedLocalProducer => + // POP of an exception in a catch block cannot be removed. For an uninitialized local, + // there should not be a consumer. We are conservative and include it here, so the + // producer would not be removed. + false + + case _: ParameterProducer => + true + + case _ => (prod.getOpcode: @switch) match { + case DUP => true + case DUP2 => prodCons.frameAt(prod).peekStack(0).getSize == 2 + case _ => InstructionStackEffect.prod(InstructionStackEffect.forAsmAnalysis(prod, prodCons.frameAt(prod))) == 1 + } + } + val prods = prodCons.producersForValueAt(cons, inputSlot) - val singleConsumer = prods forall { - case _: ExceptionProducer[_] => - false // POP of an exception in a catch block cannot be removed - case prod => - producerHasSingleOutput(prod) && { - // for DUP / DUP2, we only consider the value that is actually consumed by cons - val conss = prodCons.consumersOfValueAt(prod.getNext, inputSlot) - conss.size == 1 && conss.head == cons - } + val singleConsumer = prods forall { prod => + producerHasSingleOutput(prod) && { + // for DUP / DUP2, we only consider the value that is actually consumed by cons + val conss = prodCons.consumersOfValueAt(prod.getNext, inputSlot) + conss.size == 1 && conss.head == cons + } } if (singleConsumer) prods else Set.empty } @@ -599,12 +606,17 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { (prod.getOpcode: @switch) match { case ACONST_NULL | ICONST_M1 | ICONST_0 | ICONST_1 | ICONST_2 | ICONST_3 | ICONST_4 | ICONST_5 | LCONST_0 | LCONST_1 | FCONST_0 | FCONST_1 | FCONST_2 | DCONST_0 | DCONST_1 | - BIPUSH | SIPUSH | ILOAD | LLOAD | FLOAD | DLOAD | ALOAD | DUP => + BIPUSH | SIPUSH | ILOAD | LLOAD | FLOAD | DLOAD | ALOAD=> toRemove += prod - case DUP2 => - assert(size == 2, s"DUP2 for two size-1 values; $prodString") // ensured in method `producerHasSingleOutput` - toRemove += prod + case opc @ (DUP | DUP2) => + assert(opc != 2 || size == 2, s"DUP2 for two size-1 values; $prodString") // ensured in method `producerHasSingleOutput` + if (toRemove(prod)) + // the DUP is already scheduled for removal because one of its consumers is a POP. + // now the second consumer is also a POP, so we need to eliminate the DUP's input. + handleInputs(prod, 1) + else + toRemove += prod case DUP_X1 | DUP_X2 | DUP2_X1 | DUP2_X2 | SWAP => // these are excluded in method `producerHasSingleOutput` -- cgit v1.2.3 From 34fcee8f7dd7a9128ab7a14f97d5dd8e89ab1b4d Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 11 Dec 2015 14:43:26 +0100 Subject: Minor cleanups in eliminateStaleStores --- src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index af0223b449..7d2ae60b66 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -382,17 +382,18 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { lazy val prodCons = new ProdConsAnalyzer(method, owner) def hasNoCons(varIns: AbstractInsnNode, slot: Int) = prodCons.consumersOfValueAt(varIns.getNext, slot).isEmpty - var changed = false - // insns to delete: IINC that have no consumer val toDelete = mutable.ArrayBuffer.empty[IincInsnNode] + // xSTORE insns to be replaced by POP or POP2 val storesToDrop = mutable.ArrayBuffer.empty[VarInsnNode] + // ASTORE insn that have no consumer. // - if the local is not live, the store is replaced by POP // - otherwise, pop the argument value and store NULL instead. Unless the boolean field is // `true`: then the store argument is already known to be ACONST_NULL. val toNullOut = mutable.ArrayBuffer.empty[(VarInsnNode, Boolean)] + // `true` for variables that are known to be live val liveVars = new Array[Boolean](method.maxLocals) @@ -413,18 +414,16 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val isStoreNull = prods.size == 1 && prods.head.getOpcode == ACONST_NULL toNullOut += ((vi, isStoreNull)) } - changed = true case ii: IincInsnNode if hasNoCons(ii, ii.`var`) => toDelete += ii - changed = true case vi: VarInsnNode => - val longSize = if (isSize2LoadOrStore(vi.getOpcode)) 1 else 0 liveVars(vi.`var`) = true case ii: IincInsnNode => liveVars(ii.`var`) = true + case _ => } @@ -434,7 +433,9 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { } toDelete foreach method.instructions.remove + storesToDrop foreach replaceByPop + for ((vi, isStoreNull) <- toNullOut) { if (!liveVars(vi.`var`)) replaceByPop(vi) // can drop `ASTORE x` where x has only dead stores else { @@ -446,7 +447,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { } } - changed + toDelete.nonEmpty || storesToDrop.nonEmpty || toNullOut.nonEmpty } } -- cgit v1.2.3 From 93de014be97c5f5a707a36ce10eeaab68037fda7 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 11 Dec 2015 09:37:02 +0100 Subject: Rename test MethodLevelOpts -> MethodLevelOptsTest --- .../nsc/backend/jvm/opt/MethodLevelOpts.scala | 255 --------------------- .../nsc/backend/jvm/opt/MethodLevelOptsTest.scala | 255 +++++++++++++++++++++ 2 files changed, 255 insertions(+), 255 deletions(-) delete mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala create mode 100644 test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala deleted file mode 100644 index 3228d3de65..0000000000 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ /dev/null @@ -1,255 +0,0 @@ -package scala.tools.nsc -package backend.jvm -package opt - -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.Test -import scala.tools.asm.Opcodes._ -import org.junit.Assert._ - -import scala.tools.nsc.backend.jvm.AsmUtils._ -import scala.tools.testing.AssertUtil._ - -import CodeGenTools._ -import scala.tools.partest.ASMConverters -import ASMConverters._ -import scala.tools.testing.ClearAfterClass - -object MethodLevelOpts extends ClearAfterClass.Clearable { - var methodOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") - def clear(): Unit = { methodOptCompiler = null } -} - -@RunWith(classOf[JUnit4]) -class MethodLevelOpts extends ClearAfterClass { - ClearAfterClass.stateToClear = MethodLevelOpts - - val methodOptCompiler = MethodLevelOpts.methodOptCompiler - - def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) - - @Test - def eliminateEmptyTry(): Unit = { - val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" - val warn = "a pure expression does nothing in statement position" - assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) - } - - @Test - def eliminateLoadBoxedUnit(): Unit = { - // the compiler inserts a boxed into the try block. it's therefore non-empty (and live) and not eliminated. - val code = "def f = { try {} catch { case _: Throwable => 0 }; 1 }" - val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.length == 0) - assertSameCode(m.instructions.dropNonOp, - List(Op(ICONST_1), Op(IRETURN))) - } - - @Test - def inlineThrowInCatchNotTry(): Unit = { - // the try block does not contain the `ATHROW` instruction, but in the catch block, `ATHROW` is inlined - val code = "def f(e: Exception) = throw { try e catch { case _: Throwable => e } }" - val m = singleMethod(methodOptCompiler)(code) - assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) - assertSameCode(m.instructions, - wrapInDefault(VarOp(ALOAD, 1), Label(3), Op(ATHROW), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), VarOp(ALOAD, 1), Op(ATHROW)) - ) - } - - @Test - def inlineReturnInCatchNotTry(): Unit = { - val code = "def f: Int = return { try 1 catch { case _: Throwable => 2 } }" - // cannot inline the IRETURN into the try block (because RETURN may throw IllegalMonitorState) - val m = singleMethod(methodOptCompiler)(code) - assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) - assertSameCode(m.instructions, - wrapInDefault(Op(ICONST_1), Label(3), Op(IRETURN), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), Op(ICONST_2), Op(IRETURN))) - } - - @Test - def simplifyJumpsInTryCatchFinally(): Unit = { - val code = - """def f: Int = - | try { - | return 1 - | } catch { - | case _: Throwable => - | return 2 - | } finally { - | return 3 - | // dead - | val x = try 10 catch { case _: Throwable => 11 } - | println(x) - | } - """.stripMargin - val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.isEmpty) - assertSameCode(m.instructions.dropNonOp, List(Op(ICONST_3), Op(IRETURN))) - } - - @Test - def nullStoreLoadElim(): Unit = { - // point of this test: we have two cleanups - // - remove `ACONST_NULL; ASTORE x` if x is otherwise not live - // - remove `ASTORE x; ALOAD x` if x is otherwise not live - // in the example below, we have `ACONST_NULL; ASTORE x; ALOAD x`. in this case the store-load - // should be removed (even though it looks like a null-store at first). - val code = - """class C { - | def t = { - | val x = null - | x.toString - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - assertSameCode(getSingleMethod(c, "t").instructions.dropNonOp, - List(Op(ACONST_NULL), Invoke(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false), Op(ARETURN))) - } - - @Test - def deadStoreReferenceElim(): Unit = { - val code = - """class C { - | def t = { - | var a = "a" // assign to non-initialized, directly removed by dead store - | a = "b" // assign to initialized, replaced by null-store, which is then removed: the var is not live, the uses are null-store or store-load - | a = "c" - | a // store-load pair will be eliminated - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - assertSameCode( - getSingleMethod(c, "t").instructions.dropNonOp, - List(Ldc(LDC, "c"), Op(ARETURN))) - } - - @Test - def deadStoreReferenceKeepNull(): Unit = { - val code = - """class C { - | def t = { - | var a = "el" // this store is live, used in the println. - | println(a) - | a = "met" // since it's an ASTORE to a live variable, cannot elim the store (SI-5313), but store null instead. - | // so we get `LDC met; POP; ACONST_NULL; ASTORE 1`. the `LDC met; POP` is eliminated by push-pop. - | a = "zit" // this store is live, so we get `LDC zit; ASOTRE 1; ALOAD 1; ARETURN`. - | // we cannot eliminated the store-load sequence, because the local is live (again SI-5313). - | a - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - - assertEquals( - getSingleMethod(c, "t").instructions.dropNonOp, - List( - Ldc(LDC, "el"), VarOp(ASTORE, 1), - Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/Predef$", "println", "(Ljava/lang/Object;)V", false), - Op(ACONST_NULL), VarOp(ASTORE, 1), - Ldc(LDC, "zit"), VarOp(ASTORE, 1), VarOp(ALOAD, 1), Op(ARETURN))) - } - - @Test - def elimUnusedTupleObjectStringBox(): Unit = { - val code = - """class C { - | def t(x: Int, y: Int): Int = { - | val a = (x, y) // Tuple2$mcII$sp - | val b = (a, y) // Tuple2 - | val c = (new Object, "krik", new String) // unused java/lang/Object, java/lang/String allocation and string constant is also eliminated - | val d = new java.lang.Integer(x) - | val e = new String(new Array[Char](23)) - | val f = new scala.runtime.IntRef(11) - | x + y - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, - List(VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) - } - - @Test - def noElimImpureConstructor(): Unit = { - val code = - """class C { - | def t(x: Int, y: Int): Int = { - | val a = new java.lang.Integer("nono") - | x + y - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, - List(TypeOp(NEW, "java/lang/Integer"), Ldc(LDC, "nono"), Invoke(INVOKESPECIAL, "java/lang/Integer", "", "(Ljava/lang/String;)V", false), - VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) - } - - @Test - def elimUnusedBoxUnbox(): Unit = { - val code = - """class C { - | def t(a: Long): Int = { - | val t = 3 + a - | val u = a + t - | val v: Any = u // scala/runtime/BoxesRunTime.boxToLong - | - | val w = (v, a) // a Tuple2 (not specialized because first value is Any) - | // so calls scala/runtime/BoxesRunTime.boxToLong on the second value - | - | val x = v.asInstanceOf[Long] // scala/runtime/BoxesRunTime.unboxToLong - | - | val z = (java.lang.Long.valueOf(a), t) // java box call on the left, scala/runtime/BoxesRunTime.boxToLong on the right - | - | 0 - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, - List(Op(ICONST_0), Op(IRETURN))) - } - - @Test - def elimUnusedClosure(): Unit = { - val code = - """class C { - | def t(x: Int, y: Int): Int = { - | val f = (a: Int) => a + x + y - | val g = (b: Int) => b - x - | val h = (s: String) => println(s) - | f(30) - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - assertEquals( - getSingleMethod(c, "t").instructions.dropNonOp, - List( - IntOp(BIPUSH, 30), VarOp(ISTORE, 3), // no constant propagation, so we keep the store (and load below) of a const - VarOp(ILOAD, 1), - VarOp(ILOAD, 2), - VarOp(ILOAD, 3), - Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(III)I", false), Op(IRETURN))) - } - - @Test - def rewriteSpecializedClosureCall(): Unit = { - val code = - """class C { - | def t = { - | val f1 = (x: Int) => println(x) // int-unit specialization - | val f2 = (x: Int, y: Long) => x == y // int-long-boolean - | f1(1) - | f2(3, 4) - | } - |} - """.stripMargin - val List(c) = compileClasses(methodOptCompiler)(code) - val t = getSingleMethod(c, "t") - assert(!t.instructions.exists(_.opcode == INVOKEDYNAMIC), t) - } -} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala new file mode 100644 index 0000000000..f4524f0bb1 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -0,0 +1,255 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.nsc.backend.jvm.AsmUtils._ +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import scala.tools.testing.ClearAfterClass + +object MethodLevelOptsTest extends ClearAfterClass.Clearable { + var methodOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:method") + def clear(): Unit = { methodOptCompiler = null } +} + +@RunWith(classOf[JUnit4]) +class MethodLevelOptsTest extends ClearAfterClass { + ClearAfterClass.stateToClear = MethodLevelOptsTest + + val methodOptCompiler = MethodLevelOptsTest.methodOptCompiler + + def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) + + @Test + def eliminateEmptyTry(): Unit = { + val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" + val warn = "a pure expression does nothing in statement position" + assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) + } + + @Test + def eliminateLoadBoxedUnit(): Unit = { + // the compiler inserts a boxed into the try block. it's therefore non-empty (and live) and not eliminated. + val code = "def f = { try {} catch { case _: Throwable => 0 }; 1 }" + val m = singleMethod(methodOptCompiler)(code) + assertTrue(m.handlers.length == 0) + assertSameCode(m.instructions.dropNonOp, + List(Op(ICONST_1), Op(IRETURN))) + } + + @Test + def inlineThrowInCatchNotTry(): Unit = { + // the try block does not contain the `ATHROW` instruction, but in the catch block, `ATHROW` is inlined + val code = "def f(e: Exception) = throw { try e catch { case _: Throwable => e } }" + val m = singleMethod(methodOptCompiler)(code) + assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) + assertSameCode(m.instructions, + wrapInDefault(VarOp(ALOAD, 1), Label(3), Op(ATHROW), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), VarOp(ALOAD, 1), Op(ATHROW)) + ) + } + + @Test + def inlineReturnInCatchNotTry(): Unit = { + val code = "def f: Int = return { try 1 catch { case _: Throwable => 2 } }" + // cannot inline the IRETURN into the try block (because RETURN may throw IllegalMonitorState) + val m = singleMethod(methodOptCompiler)(code) + assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) + assertSameCode(m.instructions, + wrapInDefault(Op(ICONST_1), Label(3), Op(IRETURN), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), Op(ICONST_2), Op(IRETURN))) + } + + @Test + def simplifyJumpsInTryCatchFinally(): Unit = { + val code = + """def f: Int = + | try { + | return 1 + | } catch { + | case _: Throwable => + | return 2 + | } finally { + | return 3 + | // dead + | val x = try 10 catch { case _: Throwable => 11 } + | println(x) + | } + """.stripMargin + val m = singleMethod(methodOptCompiler)(code) + assertTrue(m.handlers.isEmpty) + assertSameCode(m.instructions.dropNonOp, List(Op(ICONST_3), Op(IRETURN))) + } + + @Test + def nullStoreLoadElim(): Unit = { + // point of this test: we have two cleanups + // - remove `ACONST_NULL; ASTORE x` if x is otherwise not live + // - remove `ASTORE x; ALOAD x` if x is otherwise not live + // in the example below, we have `ACONST_NULL; ASTORE x; ALOAD x`. in this case the store-load + // should be removed (even though it looks like a null-store at first). + val code = + """class C { + | def t = { + | val x = null + | x.toString + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t").instructions.dropNonOp, + List(Op(ACONST_NULL), Invoke(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false), Op(ARETURN))) + } + + @Test + def deadStoreReferenceElim(): Unit = { + val code = + """class C { + | def t = { + | var a = "a" // assign to non-initialized, directly removed by dead store + | a = "b" // assign to initialized, replaced by null-store, which is then removed: the var is not live, the uses are null-store or store-load + | a = "c" + | a // store-load pair will be eliminated + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode( + getSingleMethod(c, "t").instructions.dropNonOp, + List(Ldc(LDC, "c"), Op(ARETURN))) + } + + @Test + def deadStoreReferenceKeepNull(): Unit = { + val code = + """class C { + | def t = { + | var a = "el" // this store is live, used in the println. + | println(a) + | a = "met" // since it's an ASTORE to a live variable, cannot elim the store (SI-5313), but store null instead. + | // so we get `LDC met; POP; ACONST_NULL; ASTORE 1`. the `LDC met; POP` is eliminated by push-pop. + | a = "zit" // this store is live, so we get `LDC zit; ASOTRE 1; ALOAD 1; ARETURN`. + | // we cannot eliminated the store-load sequence, because the local is live (again SI-5313). + | a + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + + assertEquals( + getSingleMethod(c, "t").instructions.dropNonOp, + List( + Ldc(LDC, "el"), VarOp(ASTORE, 1), + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/Predef$", "println", "(Ljava/lang/Object;)V", false), + Op(ACONST_NULL), VarOp(ASTORE, 1), + Ldc(LDC, "zit"), VarOp(ASTORE, 1), VarOp(ALOAD, 1), Op(ARETURN))) + } + + @Test + def elimUnusedTupleObjectStringBox(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = (x, y) // Tuple2$mcII$sp + | val b = (a, y) // Tuple2 + | val c = (new Object, "krik", new String) // unused java/lang/Object, java/lang/String allocation and string constant is also eliminated + | val d = new java.lang.Integer(x) + | val e = new String(new Array[Char](23)) + | val f = new scala.runtime.IntRef(11) + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def noElimImpureConstructor(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = new java.lang.Integer("nono") + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(TypeOp(NEW, "java/lang/Integer"), Ldc(LDC, "nono"), Invoke(INVOKESPECIAL, "java/lang/Integer", "", "(Ljava/lang/String;)V", false), + VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def elimUnusedBoxUnbox(): Unit = { + val code = + """class C { + | def t(a: Long): Int = { + | val t = 3 + a + | val u = a + t + | val v: Any = u // scala/runtime/BoxesRunTime.boxToLong + | + | val w = (v, a) // a Tuple2 (not specialized because first value is Any) + | // so calls scala/runtime/BoxesRunTime.boxToLong on the second value + | + | val x = v.asInstanceOf[Long] // scala/runtime/BoxesRunTime.unboxToLong + | + | val z = (java.lang.Long.valueOf(a), t) // java box call on the left, scala/runtime/BoxesRunTime.boxToLong on the right + | + | 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, + List(Op(ICONST_0), Op(IRETURN))) + } + + @Test + def elimUnusedClosure(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val f = (a: Int) => a + x + y + | val g = (b: Int) => b - x + | val h = (s: String) => println(s) + | f(30) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals( + getSingleMethod(c, "t").instructions.dropNonOp, + List( + IntOp(BIPUSH, 30), VarOp(ISTORE, 3), // no constant propagation, so we keep the store (and load below) of a const + VarOp(ILOAD, 1), + VarOp(ILOAD, 2), + VarOp(ILOAD, 3), + Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(III)I", false), Op(IRETURN))) + } + + @Test + def rewriteSpecializedClosureCall(): Unit = { + val code = + """class C { + | def t = { + | val f1 = (x: Int) => println(x) // int-unit specialization + | val f2 = (x: Int, y: Long) => x == y // int-long-boolean + | f1(1) + | f2(3, 4) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + val t = getSingleMethod(c, "t") + assert(!t.instructions.exists(_.opcode == INVOKEDYNAMIC), t) + } +} -- cgit v1.2.3 From 60ac9ecdbd9584007d70003bf8e00c4702bbd401 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 11 Dec 2015 10:48:40 +0100 Subject: Make some testing tools from InlinerTest available to other tests --- .../scala/tools/nsc/backend/jvm/CodeGenTools.scala | 17 +++++++++++++++++ .../jvm/opt/InlinerSeparateCompilationTest.scala | 1 - .../scala/tools/nsc/backend/jvm/opt/InlinerTest.scala | 19 ------------------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala index c071745788..69c3f69380 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/CodeGenTools.scala @@ -166,6 +166,19 @@ object CodeGenTools { assertTrue(s"\nExpected: $expected\nActual : $actual", actual === expected) } + def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) + def assertNoInvoke(ins: List[Instruction]): Unit = { + assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) + } + + def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) + def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { + assert(l.exists { + case Invoke(_, `receiver`, `method`, _, _) => true + case _ => false + }, l.stringLines) + } + def getSingleMethod(classNode: ClassNode, name: String): Method = convertMethod(classNode.methods.asScala.toList.find(_.name == name).get) @@ -195,4 +208,8 @@ object CodeGenTools { implicit class MortalInstruction(val ins: Instruction) extends AnyVal { def dead: (Instruction, Boolean) = (ins, false) } + + implicit class listStringLines[T](val l: List[T]) extends AnyVal { + def stringLines = l.mkString("\n") + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala index 5c9bd1c188..44b6c7ca9e 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -22,7 +22,6 @@ object InlinerSeparateCompilationTest { @RunWith(classOf[JUnit4]) class InlinerSeparateCompilationTest { import InlinerSeparateCompilationTest._ - import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} @Test def inlnieMixedinMember(): Unit = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 75c1a0d3ad..bf10b88a24 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -36,31 +36,12 @@ object InlinerTest extends ClearAfterClass.Clearable { notPerRun foreach compiler.perRunCaches.unrecordCache def clear(): Unit = { compiler = null; inlineOnlyCompiler = null } - - implicit class listStringLines[T](val l: List[T]) extends AnyVal { - def stringLines = l.mkString("\n") - } - - def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) - def assertNoInvoke(ins: List[Instruction]): Unit = { - assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) - } - - def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) - def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { - assert(l.exists { - case Invoke(_, `receiver`, `method`, _, _) => true - case _ => false - }, l.stringLines) - } } @RunWith(classOf[JUnit4]) class InlinerTest extends ClearAfterClass { ClearAfterClass.stateToClear = InlinerTest - import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} - val compiler = InlinerTest.compiler import compiler.genBCode.bTypes._ import compiler.genBCode.bTypes.backendUtils._ -- cgit v1.2.3 From 1265e19de8073da2691fac4d52bc763091ad7b9c Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 11 Dec 2015 10:47:00 +0100 Subject: Eliminate non-escaping boxes, tuples and refs Eliminate boxes, tuples and refs that are created and used within a single method without escaping. For details on the implementation see the doc comment in class BoxUnbox. This commit also cleans up the logic of inter-dependent method-level optimizations that run until reaching a fixpoint. --- .../scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala | 902 +++++++++++++++++++++ .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 206 ++++- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 146 +++- .../scala/tools/nsc/settings/ScalaSettings.scala | 4 +- .../scala/tools/partest/ASMConverters.scala | 5 + .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 150 ++-- .../nsc/backend/jvm/opt/MethodLevelOptsTest.scala | 237 ++++++ 7 files changed, 1521 insertions(+), 129 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala new file mode 100644 index 0000000000..a8829b6e29 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala @@ -0,0 +1,902 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.annotation.tailrec +import scala.tools.asm.Type +import scala.tools.asm.Opcodes._ +import scala.tools.asm.tree._ +import scala.collection.mutable +import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ + +class BoxUnbox[BT <: BTypes](val btypes: BT) { + import btypes._ + import backendUtils._ + + /** + * Eliminate box-unbox paris within `method`. Such appear commonly after closure elimination: + * + * def t2 = { + * val f = (b: Byte, i: Int) => i + b // no specialized variant for this function type + * f(1, 2) // invokes the generic `apply` + * } + * + * The closure optimizer re-writes the `apply` call to `anonfun$adapted` method, which takes + * boxed arguments. After inlining this method, we get + * + * def t2 = { + * val a = boxByte(1) + * val b = boxInteger(2) + * val r = boxInteger(anonfun$(unboxByte(a), unboxInt(b))) + * unboxInt(r) + * } + * + * All these box/unbox operations are eliminated here. + * + * Implementation: for every box operation, find all consumers of the boxed value, then all + * producers of these consumers, repeat until reaching a fixpoint. If this results in a set of + * boxing and unboxing operations, the box can be eliminated. + * + * There are two methods for eliminating boxes: + * M1: If there is a single boxing operation, the boxed value(s) are stored into new local + * variable(s) at the allocation site. Accesses to the boxed value are re-written to reads / + * writes of these locals. Advantages: + * - supports mutable boxes (IntRef and friends) + * - supports eliminating unbox operations even if the box object needs to be created + * because it escapes (see E4) + * - works by keeping the unboxed value(s) in locals AND the box in its original form + * - only for immutable boxes: modifications to the escaped box cannot be applied to + * the local variable(s) holding the boxed value(s). + * Restriction: + * - does not work if there are multiple boxing operations (see E1) + * + * M2: If there are multiple boxing operations, the boxing operations are simply eliminated, + * leaving the unboxed value(s) on the stack. Store / load operations that previously + * acted on the box are adapted to handle the boxed type(s). If the box contains multiple + * values (or a size-2 value, which doesn't fit into locals that were used for the box), + * new local slots are used for store / load operations. Restrictions: + * - does not support re-writing writes to (mutable) boxes (see E2) + * - does not support re-writing reads of boxes that also escape (see E3) + * + * + * E1: M1 only works if there's a single boxing operation. + * def e1(b: Boolean) = { + * val i: Integer = box(10) // 10 is stored into a new local, box operation and i removed + * val j: Integer = box(20) // 20 is stored into a new local, box operation adn j removed + * val r = if (b) i else j // loads and stores of the box are eliminated, r no longer exists + * unbox(r) // cannot rewrite: we don't know which local to load + * } + * Note: the example has no write and the box does not escape, so M2 works here. + * + * E2: mutable boxes with multiple boxing operations cannot be eliminated. + * M1: see E1 + * M2: cannot replace an `IntRef` on the stack by an `Int` value on the stack, an Int on the + * stack cannot be modified. + * + * def e2(b: Boolean) = { + * val r1 = new IntRef(0) + * val r2 = new IntRef(1) + * val modRef = if (b) r1 else r2 + * modRef.elem += 10 // M1: cannot rewrite: which local to write? same as E1. + * (if (b) r1 else r2).elem += 10 // M2: cannot change an Int on the stack + * (r1.elem, r2.elem) + * } + * + * + * E3: escaping boxes with multiple boxing operations cannot be rewritten. + * M1: see E1. + * M2: at *, instead of an Integer, an Int is on the stack, but the escape method expects an + * Integer. We cannot just create a box at this point: if there are multiple escapes (or + * an escape is executed more than once), the difference could be observed (reference + * equality). + * + * def e3(b: Boolean) = { + * val i: Integer = box(1) + * val j: Integer = box(2) + * escape(if (b) i else j) // * + * unbox(if (b) i else j) + * } + * + * + * E4: M1 supports rewriting unbox operations of immutable boxes that escape + * def e4 = { + * val i: Integer = box(10) // 10 is stored into a new local, loaded as argument for the box call + * escape(i) // not changed, still loads the local i holding the box + * unbox(i) // rewritten to a pop (of the box) and a load of the local variable + * } + * + * + * E4 seems to be a bit of a corner case, but it's necessary to unblock box eliminations with + * mutual dependencies. Example: + * + * val ((a, b), c) = ((1, 2), 3) + * a + b + c + * + * generates (after a few cleanups) the following (pseudo-bytecode, ignoring primitive boxing, specialization): + * + * load 1, load 2, new Tuple2 // stack: Tuple2 + * load 3 // stack: Tuple2; Int + * val local1 = new Tuple2 + * val local2 = local1._1.asInstanceOf[Tuple2] + * val c = local1._2.asInstanceOf[Int] + * if (local2 == null) throw new MatchError(local1) + * val a = local2._1 + * val b = local2._2 + * a + b + c + * + * In order to eliminate the tuples, we first need to eliminate the outer tuple (stored in local1) + * - single box operation, so we use M1 + * - there are three consumers of the outer tuple: `local1._1`, `local1._2` and + * `new MatchError(local1)`. in the last one, the tuple escapes. + * - note that the MatchError creation is dead code: local2 is never null. However, our nullness + * analysis cannot identify this: it does not track nullness through tuple stores and loads. + * - if we re-write the non-escaping consumers of the outer tuple, but keep the tuple allocation + * and the escaping consumer, we get the follwoing: + * + * load 1, load 2 + * val newLocal1 = new Tuple2; load newLocal1 // stack: Tuple2 + * val newLocal2 = 3; load newLocal2 // stack: Tuple2; Int + * val local1 = new Tuple2 + * val local2 = newLocal1 + * val c = newLocal2 + * if (local2 == null) throw new MatchError(local1) + * val a = local2._1 + * val b = local2._2 + * a + b + c + * + * At this point, the nullness analysis sees that `local2 == null` is false, dead code elimination + * removes the `throw new MatchError(local1)`. After eliminating the allocation of the outer tuple, + * the inner tuple (stored in newLocal1) can also be eliminated. + * + * + * Special case for tuples wrt specialization: a tuple getter may box or unbox the value stored + * in the tuple: calling `_1` on a `Tuple2$mcII$sp` boxes the primitive Int stored in the tuple. + * Similarly, calling `_1$mcI$sp` on a non-specialized `Tuple2` unboxes the Integer in the tuple. + * When eliminating such getters, we have to introduce appropriate box / unbox calls. + * + * + * TODO: add new calls (box / unbox) to the call graph (not urgent) + * TODO: update the call graph because stack heights change (not urgent). + * this may also affect other optimizations, we ignored the issue so far. check how stack + * heights stored in the call graph are used. + * Note: these tasks are not urgent because the call graph is not currently used during / after + * method-local optimizations, only before to perform inlining and closure rewriting. + */ + def boxUnboxElimination(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + val toInsertBefore = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + val toDelete = mutable.Set.empty[AbstractInsnNode] + + val knownHandled = mutable.Set.empty[AbstractInsnNode] + + lazy val prodCons = new ProdConsAnalyzer(method, owner) + + var nextLocal = method.maxLocals + def getLocal(size: Int) = { + val r = nextLocal + nextLocal += size + r + } + + var maxStackGrowth = 0 + + /** Mehtod M1 for eliminating box-unbox pairs (see doc comment in the beginning of this file) */ + def replaceBoxOperationsSingleCreation(creation: BoxCreation, finalCons: Set[BoxConsumer], boxKind: BoxKind, keepBox: Boolean): Unit = { + /** + * If the box is eliminated, all copy operations (loads, stores, others) of the box need to + * be removed. This method returns all copy operations that should be removed. + * + * Returns `None` in case some exotic copy operation is found that cannot be removed + * (DUP2_X1 and friends - these are never emitted by scalac). In this case, the box cannot + * be eliminated. + */ + def copyOpsToEliminate: Option[Set[AbstractInsnNode]] = { + var elidableCopyOps = Set.empty[AbstractInsnNode] + var replaceOK = true + val copyOps = new CopyOpsIterator(Set(creation), finalCons, prodCons) + while (replaceOK && copyOps.hasNext) copyOps.next() match { + case vi: VarInsnNode => + elidableCopyOps += vi + + case copyOp if copyOp.getOpcode == DUP => + elidableCopyOps += copyOp + + case _ => + replaceOK = false + } + if (replaceOK) Some(elidableCopyOps) else None + } + + val canRewrite = keepBox || (copyOpsToEliminate match { + case Some(copyOps) => + toDelete ++= copyOps + true + + case _ => false + }) + + if (canRewrite) { + val localSlots: Vector[(Int, Type)] = boxKind.boxedTypes.map(tp => (getLocal(tp.getSize), tp))(collection.breakOut) + + // store boxed value(s) into localSlots + val storeOps = localSlots.toList reverseMap { case (slot, tp) => + new VarInsnNode(tp.getOpcode(ISTORE), slot) + } + val storeInitialValues = creation.loadInitialValues match { + case Some(ops) => ops ::: storeOps + case None => storeOps + } + if (keepBox) { + val loadOps: List[VarInsnNode] = localSlots.map({ case (slot, tp) => + new VarInsnNode(tp.getOpcode(ILOAD), slot) + })(collection.breakOut) + toInsertBefore(creation.valuesConsumer) = storeInitialValues ::: loadOps + } else { + toReplace(creation.valuesConsumer) = storeInitialValues + toDelete ++= creation.allInsns - creation.valuesConsumer + } + + // rewrite consumers + finalCons foreach { + case write: StaticSetterOrInstanceWrite => + assert(!keepBox, s"cannot eliminate box write if the box remains (and escapes): $write") + val (slot, tp) = localSlots(boxKind.extractedValueIndex(write)) + val storeOp = new VarInsnNode(tp.getOpcode(ISTORE), slot) + toReplace(write.consumer) = List(storeOp) + + case c: EscapingConsumer => + assert(keepBox, s"found escaping consumer, but box is eliminated: $c") + + case extraction => + val (slot, tp) = localSlots(boxKind.extractedValueIndex(extraction)) + val loadOps = new VarInsnNode(tp.getOpcode(ILOAD), slot) :: extraction.postExtractionAdaptationOps(tp) + if (keepBox) toReplace(extraction.consumer) = getPop(1) :: loadOps + else toReplace(extraction.consumer) = loadOps + toDelete ++= extraction.allInsns - extraction.consumer + } + } + } + + /** Method M2 for eliminating box-unbox pairs (see doc comment in the beginning of this file) */ + def replaceBoxOperationsMultipleCreations(allCreations: Set[BoxCreation], allConsumers: Set[BoxConsumer], boxKind: BoxKind): Unit = { + /** + * If a single-value size-1 box is eliminated, local variables slots holding the box are + * reused to hold the unboxed value. In case there's an entry for that local variable in the + * method's local variables table (debug info), adapt the type. + * + * If there are multiple entries for a local variable that's changing types, then all + * entries for that variable are deleted - it's not obvious how to find the correct entry. + * Note that scalac never re-uses local variable slots for non-overlapping locals. Also note + * that all locals that are newly created during the optimizer don't have an entry either. + * + * Finally, note that variables that become unused are removed later from the table by + * removeUnusedLocalVariableNodes in LocalOpt. + * + * Unlike modifications that affect the method's instructions (which uses toReplace etc), + * we can directly modify the local variable table - it does not affect the frames of the + * ProdCons analysis. + */ + def updateLocalVariableTypes(reTypedLocals: Map[Int, Type]): Unit = { + lazy val localsByIndex = method.localVariables.asScala.groupBy(_.index) + for ((index, tp) <- reTypedLocals) localsByIndex.get(index).map(_.toList) match { + case Some(List(local)) => + local.desc = tp.getDescriptor + case Some(locals) => + locals foreach method.localVariables.remove + case _ => + } + } + + /** Remove box creations - leave the boxed value(s) on the stack instead. */ + def replaceCreationOps(): Unit = { + for (creation <- allCreations) creation.loadInitialValues match { + case None => + toDelete ++= creation.allInsns + + case Some(ops) => + toReplace(creation.valuesConsumer) = ops + toDelete ++= (creation.allInsns - creation.valuesConsumer) + } + } + + /** + * Replace a value extraction operation. For a single-value box, the extraction operation can + * just be removed. An extraction from a multi-value box is replaced by POP operations for the + * non-used values, and an xSTORE / xLOAD for the extracted value. Example: tuple3._2 becomes + * POP; xSTORE n; POP; xLOAD n. + */ + def replaceExtractionOps(): Unit = { + if (boxKind.boxedTypes.lengthCompare(1) == 0) { + // fast path for single-value boxes + allConsumers.foreach(extraction => extraction.postExtractionAdaptationOps(boxKind.boxedTypes.head) match { + case Nil => + toDelete ++= extraction.allInsns + case ops => + toReplace(extraction.consumer) = ops + toDelete ++= extraction.allInsns - extraction.consumer + }) + } else { + for (extraction <- allConsumers) { + val valueIndex = boxKind.extractedValueIndex(extraction) + val replacementOps = if (valueIndex == 0) { + val pops = boxKind.boxedTypes.tail.map(t => getPop(t.getSize)) + pops ::: extraction.postExtractionAdaptationOps(boxKind.boxedTypes.head) + } else { + var loadOps: List[AbstractInsnNode] = null + val consumeStack = boxKind.boxedTypes.zipWithIndex reverseMap { + case (tp, i) => + if (i == valueIndex) { + val resultSlot = getLocal(tp.getSize) + loadOps = new VarInsnNode(tp.getOpcode(ILOAD), resultSlot) :: extraction.postExtractionAdaptationOps(tp) + new VarInsnNode(tp.getOpcode(ISTORE), resultSlot) + } else { + getPop(tp.getSize) + } + } + consumeStack ::: loadOps + } + toReplace(extraction.consumer) = replacementOps + toDelete ++= extraction.allInsns - extraction.consumer + } + } + } + + checkCopyOpReplacements(allCreations, allConsumers, boxKind.boxedTypes, nextLocal, prodCons) match { + case Some((replacements, nextCopyOpLocal, reTypedLocals)) => + toReplace ++= replacements + updateLocalVariableTypes(reTypedLocals) + nextLocal = nextCopyOpLocal + replaceCreationOps() + replaceExtractionOps() + // Conservative (safe) value for stack growth. In every frame that initially has a multi-value + // box on the stack, the stack now contains all of the individual values. So for every eliminated + // box, the maxStack may be up to N-1 slots larger. + maxStackGrowth += boxKind.boxedTypes.length - 1 + + case None => + } + } + + val it = method.instructions.iterator + while (it.hasNext) { + val insn = it.next() + if (!knownHandled(insn)) BoxKind.valueCreationKind(insn, prodCons) match { + case Some((boxCreation, boxKind)) => + allCreationsConsumers(boxCreation, boxKind, prodCons) match { + case Some((allCreations, allConsumers)) => + val (escapingConsumers, boxConsumers) = allConsumers.partition(_.isEscaping) + if (boxConsumers.nonEmpty) { + for (c <- allCreations) knownHandled ++= c.allInsns + for (e <- allConsumers) knownHandled ++= e.allInsns + + val hasEscaping = escapingConsumers.nonEmpty + val hasWrite = allConsumers.exists(_.isWrite) + if (!hasEscaping && !hasWrite) { + // M2 -- see doc comment in the beginning of this file + // If both M1 and M2 can be applied, we prefer M2 because it doesn't introduce new locals. + replaceBoxOperationsMultipleCreations(allCreations, allConsumers, boxKind) + } else if (allCreations.size == 1 && (!hasEscaping || !boxKind.isMutable)) { + // M1 -- see doc comment in the beginning of this file + replaceBoxOperationsSingleCreation(allCreations.head, allConsumers, boxKind, keepBox = hasEscaping) + } + } + + case None => + } + + case None => + } + } + + def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match { + case mi: MethodInsnNode => callGraph.removeCallsite(mi, method) + case _ => + } + + for ((location, ops) <- toInsertBefore; op <- ops) + method.instructions.insertBefore(location, op) + + for ((oldOp, newOps) <- toReplace) { + for (newOp <- newOps) method.instructions.insertBefore(oldOp, newOp) + method.instructions.remove(oldOp) + removeFromCallGraph(oldOp) + } + + for (op <- toDelete) { + method.instructions.remove(op) + removeFromCallGraph(op) + } + + method.maxLocals = nextLocal + method.maxStack += maxStackGrowth + toInsertBefore.nonEmpty || toReplace.nonEmpty || toDelete.nonEmpty + } + } + + /** + * Given a box creations operation + * - find all ultimate consumers for the produced value. then: + * - for all consumed values, find all producer operations. check that all are box creations + * - recurse until reaching a fixpoint + * + * Returns a set of box creations and a set of box consumers. Note that the box consumers may + * contain [[EscapingConsumer]]s, even if there are multiple box creation operations. The callee + * will handle this case (and not attempt to eliminate the box). + */ + def allCreationsConsumers(initialCreation: BoxCreation, boxKind: BoxKind, prodCons: ProdConsAnalyzer): Option[(Set[BoxCreation], Set[BoxConsumer])] = { + var creations = Set(initialCreation) + var consumers = Set.empty[BoxConsumer] + + def addCreations(boxConsumer: BoxConsumer): Boolean = { + val newProds = boxConsumer.boxProducers(prodCons).filterNot(prod => creations.exists(_.producer == prod)) + newProds.forall(prod => boxKind.checkBoxCreation(prod, prodCons) match { + case Some(boxCreation) => + creations += boxCreation + addBoxConsumers(boxCreation) + + case _ => false + }) + } + + def addBoxConsumers(creation: BoxCreation): Boolean = { + val newCons = creation.boxConsumers(prodCons, ultimate = true).filterNot(cons => consumers.exists(_.consumer == cons)) + newCons.forall(cons => boxKind.checkBoxConsumer(cons, prodCons) match { + case Some(boxConsumer) => + consumers += boxConsumer + addCreations(boxConsumer) + + case _ => + creations.size <= 1 && { + // If there's a single box creation, the box operations can still be rewritten + consumers += EscapingConsumer(cons) + true + } + }) + } + + if (addBoxConsumers(initialCreation)) Some((creations, consumers)) + else None + } + + /** + * Takes two sets `initialProds` and `finalCons` such that all boxes produced by the first set + * are only consumed by an operation in the second set. + * + * Returns a map that replaces copy operations (ALOAD / ASTORE) between the producers and + * consumers with corresponding copy operations for the values stored in the box. The returned + * `Int` value returns the next free local variable slot. + * + * Examples: + * - for an Integer box, an ASTORE x is simply replaced by ISTORE x + * - for a pair of two references, an ASTORE x is replaced by `ASTORE x1; ASTORE x2` where x1 + * and x2 are fresh locals + * + * Not all copy operations can be supported: DUP only works for single-value boxes, the more + * exotic copy operations (DUP2_X2) are not supported (note that Scalac never emits them). If a + * copy operation cannot be replaced, this method returns `None`. + */ + def checkCopyOpReplacements(initialProds: Set[BoxCreation], finalCons: Set[BoxConsumer], valueTypes: List[Type], nextLocal: Int, prodCons: ProdConsAnalyzer): Option[(Map[AbstractInsnNode, List[AbstractInsnNode]], Int, Map[Int, Type])] = { + var replacements = Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + var reTypedLocals = Map.empty[Int, Type] + + var nextCopyOpLocal = nextLocal + val newLocalsMap: mutable.LongMap[List[(Type, Int)]] = mutable.LongMap.empty + def newLocals(index: Int) = newLocalsMap.getOrElseUpdate(index, valueTypes match { + case List(t) if t.getSize == 1 => + reTypedLocals += index -> t + List((t, index)) + case _ => valueTypes.map(t => { + val newIndex = nextCopyOpLocal + nextCopyOpLocal += t.getSize + (t, newIndex) + }) + }) + + var replaceOK = true + val copyOps = new CopyOpsIterator(initialProds, finalCons, prodCons) + while (replaceOK && copyOps.hasNext) copyOps.next() match { + case vi: VarInsnNode => + val isLoad = vi.getOpcode == ALOAD + val typedVarOp = (tp: (Type, Int)) => { + val opc = tp._1.getOpcode(if (isLoad) ILOAD else ISTORE) + new VarInsnNode(opc, tp._2) + } + val locs = newLocals(vi.`var`) + replacements += vi -> (if (isLoad) locs.map(typedVarOp) else locs.reverseMap(typedVarOp)) + + case copyOp => + if (copyOp.getOpcode == DUP && valueTypes.lengthCompare(1) == 0) { + if (valueTypes.head.getSize == 2) + replacements += copyOp -> List(new InsnNode(DUP2)) + } else { + replaceOK = false + } + } + if (replaceOK) Some((replacements, nextCopyOpLocal, reTypedLocals)) else None + } + + /** + * For a set of box creation operations and a corresponding set of box consumer operations, + * this iterator returns all copy operations (load, store, dup) that are in between. + */ + class CopyOpsIterator(initialCreations: Set[BoxCreation], finalCons: Set[BoxConsumer], prodCons: ProdConsAnalyzer) extends Iterator[AbstractInsnNode] { + private var queue = mutable.Queue.empty[AbstractInsnNode] ++ initialCreations.iterator.flatMap(_.boxConsumers(prodCons, ultimate = false)) + + // a single copy operation can consume multiple producers: val a = if (b) box(1) else box(2). + // the `ASTORE a` has two producers (the two box operations). we need to handle it only once. + private val visited = mutable.Set.empty[AbstractInsnNode] + + private val boxConsumingOps = finalCons.map(_.consumer) + + @tailrec private def advanceToNextCopyOp(): Unit = { + if (queue.nonEmpty) { + val h = queue.front + if (visited(h) || boxConsumingOps(h)) { + queue.dequeue() + advanceToNextCopyOp() + } + } + } + + def hasNext: Boolean = { + advanceToNextCopyOp() + queue.nonEmpty + } + + def next(): AbstractInsnNode = { + advanceToNextCopyOp() + val r = queue.dequeue() + visited += r + queue ++= prodCons.consumersOfOutputsFrom(r) + r + } + } + + trait BoxKind { + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] + def boxedTypes: List[Type] + def extractedValueIndex(extraction: BoxConsumer): Int + def isMutable: Boolean + } + + object BoxKind { + def valueCreationKind(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[(BoxCreation, BoxKind)] = { + PrimitiveBox.checkPrimitiveBox(insn, None, prodCons) orElse + Ref.checkRefCreation(insn, None, prodCons) orElse + Tuple.checkTupleCreation(insn, None, prodCons) + } + + /** + * Check if `newOp` is part of a standard object construction pattern in which: + * + * NEW T + * DUP + * [load constructor args] + * INVOKESPECIAL T.init + * + * The method ensures that the entire construction pattern is closed in itself, without any + * branches going in or out. This is checked by looking at producers / consumers: + * - `DUP` is the only consumer of `NEW`, and vice versa + * - `DUP` the only producer for the receiver of the constructor call + * - The set of consumers of `DUP` without the constructor call is the same as + * the set of consumers of the value on the stack top after the constructor call + */ + def checkInstanceCreation(newOp: TypeInsnNode, prodCons: ProdConsAnalyzer): Option[(InsnNode, MethodInsnNode)] = { + val newCons = prodCons.consumersOfOutputsFrom(newOp) + if (newCons.size == 1 && newCons.head.getOpcode == DUP) { + val dupOp = newCons.head.asInstanceOf[InsnNode] + if (prodCons.producersForInputsOf(dupOp) == Set(newOp)) { + val dupCons = prodCons.consumersOfOutputsFrom(dupOp) + val initCalls = dupCons collect { + case mi: MethodInsnNode if mi.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME && mi.owner == newOp.desc => mi + } + if (initCalls.size == 1) { + val initCall = initCalls.head + val numArgs = Type.getArgumentTypes(initCall.desc).length + val receiverProds = prodCons.producersForValueAt(initCall, prodCons.frameAt(initCall).stackTop - numArgs) + if (receiverProds == Set(dupOp)) { + val dupConsWithoutInit = dupCons - initCall + val afterInit = initCall.getNext + val stackTopAfterInit = prodCons.frameAt(afterInit).stackTop + val initializedInstanceCons = prodCons.consumersOfValueAt(afterInit, stackTopAfterInit) + if (initializedInstanceCons == dupConsWithoutInit && prodCons.producersForValueAt(afterInit, stackTopAfterInit) == Set(dupOp)) { + return Some((dupOp, initCall)) + } + } + } + } + } + None + } + + /** + * If `mi` is an invocation of a method on Predef, check if the receiver is a GETSTATIC of + * Predef.MODULE$ and return it. + */ + def checkReceiverPredefLoad(mi: MethodInsnNode, prodCons: ProdConsAnalyzer): Option[AbstractInsnNode] = { + val numArgs = Type.getArgumentTypes(mi.desc).length + val receiverProds = prodCons.producersForValueAt(mi, prodCons.frameAt(mi).stackTop - numArgs) + if (receiverProds.size == 1) { + val prod = receiverProds.head + if (isPredefLoad(prod) && prodCons.consumersOfOutputsFrom(prod) == Set(mi)) return Some(prod) + } + None + } + } + + case class PrimitiveBox(boxedType: Type, boxClass: InternalName) extends BoxKind { + import PrimitiveBox._ + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkPrimitiveBox(insn, Some(this), prodCons).map(_._1) + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkPrimitiveUnbox(insn, this, prodCons) + def boxedTypes: List[Type] = List(boxedType) + def extractedValueIndex(extraction: BoxConsumer): Int = 0 + def isMutable = false + } + + object PrimitiveBox { + private def boxedType(mi: MethodInsnNode) = Type.getArgumentTypes(mi.desc)(0) + + private def boxClass(mi: MethodInsnNode) = { + if (mi.name == GenBCode.INSTANCE_CONSTRUCTOR_NAME) mi.owner + else Type.getReturnType(mi.desc).getInternalName + } + + def checkPrimitiveBox(insn: AbstractInsnNode, expectedKind: Option[PrimitiveBox], prodCons: ProdConsAnalyzer): Option[(BoxCreation, PrimitiveBox)] = { + // mi is either a box factory or a box constructor invocation + def checkKind(mi: MethodInsnNode) = expectedKind match { + case Some(kind) => if (kind.boxClass == boxClass(mi)) expectedKind else None + case None => Some(PrimitiveBox(boxedType(mi), boxClass(mi))) + } + + insn match { + case mi: MethodInsnNode => + if (isScalaBox(mi) || isJavaBox(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) + else if (isPredefAutoBox(mi)) + for (predefLoad <- BoxKind.checkReceiverPredefLoad(mi, prodCons); kind <- checkKind(mi)) + yield (ModuleFactory(predefLoad, mi), kind) + else None + + case ti: TypeInsnNode if ti.getOpcode == NEW => + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isPrimitiveBoxConstructor(initCall); kind <- checkKind(initCall)) + yield (InstanceCreation(ti, dupOp, initCall), kind) + + case _ => None + } + } + + def checkPrimitiveUnbox(insn: AbstractInsnNode, kind: PrimitiveBox, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = { + def typeOK(mi: MethodInsnNode) = kind.boxedType == Type.getReturnType(mi.desc) + insn match { + case mi: MethodInsnNode => + if ((isScalaUnbox(mi) || isJavaUnbox(mi)) && typeOK(mi)) Some(StaticGetterOrInstanceRead(mi)) + else if (isPredefAutoUnbox(mi) && typeOK(mi)) BoxKind.checkReceiverPredefLoad(mi, prodCons).map(ModuleGetter(_, mi)) + else None + + case _ => None + } + } + } + + case class Ref(boxedType: Type, refClass: InternalName) extends BoxKind { + import Ref._ + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkRefCreation(insn, Some(this), prodCons).map(_._1) + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkRefConsumer(insn, this, prodCons) + def boxedTypes: List[Type] = List(boxedType) + def extractedValueIndex(extraction: BoxConsumer): Int = 0 + def isMutable = true + } + + object Ref { + private def boxedType(mi: MethodInsnNode): Type = runtimeRefClassBoxedType(mi.owner) + private def refClass(mi: MethodInsnNode): InternalName = mi.owner + private def loadZeroValue(refZeroCall: MethodInsnNode): List[AbstractInsnNode] = List(loadZeroForTypeSort(runtimeRefClassBoxedType(refZeroCall.owner).getSort)) + + def checkRefCreation(insn: AbstractInsnNode, expectedKind: Option[Ref], prodCons: ProdConsAnalyzer): Option[(BoxCreation, Ref)] = { + def checkKind(mi: MethodInsnNode): Option[Ref] = expectedKind match { + case Some(kind) => if (kind.refClass == refClass(mi)) expectedKind else None + case None => Some(Ref(boxedType(mi), refClass(mi))) + } + + insn match { + case mi: MethodInsnNode => + if (isRefCreate(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = None), _)) + else if (isRefZero(mi)) checkKind(mi).map((StaticFactory(mi, loadInitialValues = Some(loadZeroValue(mi))), _)) + else None + + case ti: TypeInsnNode if ti.getOpcode == NEW => + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isRuntimeRefConstructor(initCall); kind <- checkKind(initCall)) + yield (InstanceCreation(ti, dupOp, initCall), kind) + + case _ => None + } + } + + def checkRefConsumer(insn: AbstractInsnNode, kind: Ref, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = insn match { + case fi: FieldInsnNode if fi.owner == kind.refClass && fi.name == "elem" => + if (fi.getOpcode == GETFIELD) Some(StaticGetterOrInstanceRead(fi)) + else if (fi.getOpcode == PUTFIELD) Some(StaticSetterOrInstanceWrite(fi)) + else None + + case _ => None + } + } + + case class Tuple(boxedTypes: List[Type], tupleClass: InternalName) extends BoxKind { + import Tuple._ + def checkBoxCreation(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxCreation] = checkTupleCreation(insn, Some(this), prodCons).map(_._1) + def checkBoxConsumer(insn: AbstractInsnNode, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = checkTupleExtraction(insn, this, prodCons) + def extractedValueIndex(extraction: BoxConsumer): Int = extraction match { + case StaticGetterOrInstanceRead(mi: MethodInsnNode) => tupleGetterIndex(mi.name) + case PrimitiveBoxingGetter(mi) => tupleGetterIndex(mi.name) + case PrimitiveUnboxingGetter(mi, _) => tupleGetterIndex(mi.name) + case _ => throw new AssertionError(s"Expected tuple getter, found $extraction") + } + def isMutable = false + } + + object Tuple { + private def boxedTypes(mi: MethodInsnNode): List[Type] = Type.getArgumentTypes(mi.desc).toList + private def tupleClass(mi: MethodInsnNode): InternalName = mi.owner + + def checkTupleCreation(insn: AbstractInsnNode, expectedKind: Option[Tuple], prodCons: ProdConsAnalyzer): Option[(BoxCreation, Tuple)] = { + def checkKind(mi: MethodInsnNode): Option[Tuple] = expectedKind match { + case Some(kind) => if (kind.tupleClass == tupleClass(mi)) expectedKind else None + case None => Some(Tuple(boxedTypes(mi), tupleClass(mi))) + } + + insn match { + // no need to check for TupleN.apply: the compiler transforms case companion apply calls to constructor invocations + case ti: TypeInsnNode if ti.getOpcode == NEW => + for ((dupOp, initCall) <- BoxKind.checkInstanceCreation(ti, prodCons) if isTupleConstructor(initCall); kind <- checkKind(initCall)) + yield (InstanceCreation(ti, dupOp, initCall), kind) + + case _ => None + } + } + + private def isSpecializedTupleClass(tupleClass: InternalName) = tupleClass matches "scala/Tuple[12]\\$mc[IJDCZ]{1,2}\\$sp" + private def isSpecializedTupleGetter(mi: MethodInsnNode) = mi.name matches "_[12]\\$mc[IJDCZ]\\$sp" + private def isTupleGetter(mi: MethodInsnNode) = mi.name matches "_\\d\\d?" + + def checkTupleExtraction(insn: AbstractInsnNode, kind: Tuple, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = { + val expectedTupleClass = kind.tupleClass + insn match { + case mi: MethodInsnNode => + val tupleClass = mi.owner + if (isSpecializedTupleClass(expectedTupleClass)) { + val typeOK = tupleClass == expectedTupleClass || tupleClass == expectedTupleClass.substring(0, expectedTupleClass.indexOf('$')) + if (typeOK) { + if (isSpecializedTupleGetter(mi)) return Some(StaticGetterOrInstanceRead(mi)) + else if (isTupleGetter(mi)) return Some(PrimitiveBoxingGetter(mi)) + } + } else if (expectedTupleClass == tupleClass) { + if (isSpecializedTupleGetter(mi)) return Some(PrimitiveUnboxingGetter(mi, Type.getReturnType(mi.desc))) + else if (isTupleGetter(mi)) return Some(StaticGetterOrInstanceRead(mi)) + } + + case _ => + } + None + } + + private val getterIndexPattern = "_(\\d{1,2}).*".r + def tupleGetterIndex(getterName: String) = getterName match { case getterIndexPattern(i) => i.toInt - 1 } + } + + // TODO: add more + // case class ValueClass(valueClass: Type, valueType: Type) extends BoxKind + + sealed trait BoxCreation { + // to support box creation operations that don't consume an initial value from the stack, e.g., IntRef.zero + val loadInitialValues: Option[List[AbstractInsnNode]] + + /** + * The instruction that produces the box value; for instance creations, the `NEW` operation. + */ + def producer: AbstractInsnNode + + /** + * The instruction that consumes the boxed values; for instance creations, the `init` call. + */ + def valuesConsumer: MethodInsnNode = this match { + case StaticFactory(call, _) => call + case ModuleFactory(_, call) => call + case InstanceCreation(_, _, initCall) => initCall + } + + def allInsns: Set[AbstractInsnNode] = this match { + case StaticFactory(c, _) => Set(c) + case ModuleFactory(m, c) => Set(m, c) + case InstanceCreation(n, d, i) => Set(n, d, i) + } + + /** + * The consumers of the box produced by this box creation. If `ultimate` is true, then the + * final consumers are returned (e.g., an unbox operation), otherwise direct consumers (e.g., + * a store operation). + */ + def boxConsumers(prodCons: ProdConsAnalyzer, ultimate: Boolean): Set[AbstractInsnNode] = { + val startInsn = this match { + // for the non-transitive case (ultimate == false), it's important to start at the `dupOp`, + // not the `newOp` - look at the BoxCreation as a black box, get its consumers. + case InstanceCreation(_, dupOp, _) => dupOp + case _ => producer + } + val cons = if (ultimate) prodCons.ultimateConsumersOfOutputsFrom(startInsn) else prodCons.consumersOfOutputsFrom(startInsn) + this match { + case InstanceCreation(_, _, initCall) => cons - initCall + case _ => cons + } + } + } + + case class StaticFactory(producer: MethodInsnNode, loadInitialValues: Option[List[AbstractInsnNode]]) extends BoxCreation + case class ModuleFactory(moduleLoad: AbstractInsnNode, producer: MethodInsnNode) extends BoxCreation { + val loadInitialValues: Option[List[AbstractInsnNode]] = None + } + case class InstanceCreation(newOp: TypeInsnNode, dupOp: InsnNode, initCall: MethodInsnNode) extends BoxCreation { + def producer = newOp + val loadInitialValues: Option[List[AbstractInsnNode]] = None + } + + sealed trait BoxConsumer { + val consumer: AbstractInsnNode + + def allInsns: Set[AbstractInsnNode] = this match { + case ModuleGetter(m, c) => Set(m, c) + case _ => Set(consumer) + } + + /** + * The initial producers of the box value consumed by this box consumer + */ + def boxProducers(prodCons: ProdConsAnalyzer): Set[AbstractInsnNode] = { + val stackTop = prodCons.frameAt(consumer).stackTop + val slot = if (isWrite) stackTop - 1 else stackTop + prodCons.initialProducersForValueAt(consumer, slot) + } + + def isEscaping = this match { + case _: EscapingConsumer => true + case _ => false + } + + def isWrite = this match { + case _: StaticSetterOrInstanceWrite => true + case _ => false + } + + /** + * If this box consumer extracts a boxed value and applies a conversion, this method returns + * equivalent conversion operations. For example, invoking `_1$mcI$sp` on a non-specialized + * `Tuple2` extracts the Integer value and unboxes it. + */ + def postExtractionAdaptationOps(typeOfExtractedValue: Type): List[AbstractInsnNode] = this match { + case PrimitiveBoxingGetter(_) => List(getScalaBox(typeOfExtractedValue)) + case PrimitiveUnboxingGetter(_, unboxedPrimitive) => List(getScalaUnbox(unboxedPrimitive)) + case _ => Nil + } + } + + /** Static extractor (BoxesRunTime.unboxToInt) or GETFIELD or getter invocation */ + case class StaticGetterOrInstanceRead(consumer: AbstractInsnNode) extends BoxConsumer + /** A getter that boxes the returned value, e.g., `Tuple2$mcII$sp._1` */ + case class PrimitiveBoxingGetter(consumer: MethodInsnNode) extends BoxConsumer + /** A getter that unboxes the returned value, e.g., `Tuple2._1$mcI$sp` */ + case class PrimitiveUnboxingGetter(consumer: MethodInsnNode, unboxedPrimitive: Type) extends BoxConsumer + /** An extractor method in a Scala module, e.g., `Predef.Integer2int` */ + case class ModuleGetter(moduleLoad: AbstractInsnNode, consumer: MethodInsnNode) extends BoxConsumer + /** PUTFIELD or setter invocation */ + case class StaticSetterOrInstanceWrite(consumer: AbstractInsnNode) extends BoxConsumer + /** An unknown box consumer */ + case class EscapingConsumer(consumer: AbstractInsnNode) extends BoxConsumer +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index d7ef13cb9c..ebc6fa1c76 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -196,6 +196,18 @@ object BytecodeUtils { def instructionResultSize(insn: AbstractInsnNode) = InstructionStackEffect.prod(InstructionStackEffect.forClassfile(insn)) + def loadZeroForTypeSort(sort: Int) = (sort: @switch) match { + case Type.BOOLEAN | + Type.BYTE | + Type.CHAR | + Type.SHORT | + Type.INT => new InsnNode(ICONST_0) + case Type.LONG => new InsnNode(LCONST_0) + case Type.FLOAT => new InsnNode(FCONST_0) + case Type.DOUBLE => new InsnNode(DCONST_0) + case Type.OBJECT => new InsnNode(ACONST_NULL) + } + /** * The number of local variable slots used for parameters and for the `this` reference. */ @@ -342,39 +354,55 @@ object BytecodeUtils { isSideEffectFreeConstructorCall(insn) } - private val srBoxesRuntimeName = "scala/runtime/BoxesRunTime" + private val srBoxesRunTimeName = "scala/runtime/BoxesRunTime" + + private val boxToMethods = Map( + Type.BOOLEAN -> ("boxToBoolean", "(Z)Ljava/lang/Boolean;"), + Type.BYTE -> ("boxToByte", "(B)Ljava/lang/Byte;"), + Type.CHAR -> ("boxToCharacter", "(C)Ljava/lang/Character;"), + Type.SHORT -> ("boxToShort", "(S)Ljava/lang/Short;"), + Type.INT -> ("boxToInteger", "(I)Ljava/lang/Integer;"), + Type.LONG -> ("boxToLong", "(J)Ljava/lang/Long;"), + Type.FLOAT -> ("boxToFloat", "(F)Ljava/lang/Float;"), + Type.DOUBLE -> ("boxToDouble", "(D)Ljava/lang/Double;")) def isScalaBox(insn: MethodInsnNode): Boolean = { - insn.owner == srBoxesRuntimeName && { + insn.owner == srBoxesRunTimeName && { val args = Type.getArgumentTypes(insn.desc) - args.length == 1 && ((args(0).getSort: @switch) match { - case Type.BOOLEAN => insn.name == "boxToBoolean" && insn.desc == "(Z)Ljava/lang/Boolean;" - case Type.BYTE => insn.name == "boxToByte" && insn.desc == "(B)Ljava/lang/Byte;" - case Type.CHAR => insn.name == "boxToCharacter" && insn.desc == "(C)Ljava/lang/Character;" - case Type.SHORT => insn.name == "boxToShort" && insn.desc == "(S)Ljava/lang/Short;" - case Type.INT => insn.name == "boxToInteger" && insn.desc == "(I)Ljava/lang/Integer;" - case Type.LONG => insn.name == "boxToLong" && insn.desc == "(J)Ljava/lang/Long;" - case Type.FLOAT => insn.name == "boxToFloat" && insn.desc == "(F)Ljava/lang/Float;" - case Type.DOUBLE => insn.name == "boxToDouble" && insn.desc == "(D)Ljava/lang/Double;" + args.length == 1 && (boxToMethods.get(args(0).getSort) match { + case Some((name, desc)) => name == insn.name && desc == insn.desc case _ => false }) } } + def getScalaBox(primitiveType: Type): MethodInsnNode = { + val (method, desc) = boxToMethods(primitiveType.getSort) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeName, method, desc, /*itf =*/ false) + } + + private val unboxToMethods = Map( + Type.BOOLEAN -> ("unboxToBoolean", "(Ljava/lang/Object;)Z"), + Type.BYTE -> ("unboxToByte", "(Ljava/lang/Object;)B"), + Type.CHAR -> ("unboxToChar", "(Ljava/lang/Object;)C"), + Type.SHORT -> ("unboxToShort", "(Ljava/lang/Object;)S"), + Type.INT -> ("unboxToInt", "(Ljava/lang/Object;)I"), + Type.LONG -> ("unboxToLong", "(Ljava/lang/Object;)J"), + Type.FLOAT -> ("unboxToFloat", "(Ljava/lang/Object;)F"), + Type.DOUBLE -> ("unboxToDouble", "(Ljava/lang/Object;)D")) + def isScalaUnbox(insn: MethodInsnNode): Boolean = { - insn.owner == srBoxesRuntimeName && ((Type.getReturnType(insn.desc).getSort: @switch) match { - case Type.BOOLEAN => insn.name == "unboxToBoolean" && insn.desc == "(Ljava/lang/Object;)Z" - case Type.BYTE => insn.name == "unboxToByte" && insn.desc == "(Ljava/lang/Object;)B" - case Type.CHAR => insn.name == "unboxToChar" && insn.desc == "(Ljava/lang/Object;)C" - case Type.SHORT => insn.name == "unboxToShort" && insn.desc == "(Ljava/lang/Object;)S" - case Type.INT => insn.name == "unboxToInt" && insn.desc == "(Ljava/lang/Object;)I" - case Type.LONG => insn.name == "unboxToLong" && insn.desc == "(Ljava/lang/Object;)J" - case Type.FLOAT => insn.name == "unboxToFloat" && insn.desc == "(Ljava/lang/Object;)F" - case Type.DOUBLE => insn.name == "unboxToDouble" && insn.desc == "(Ljava/lang/Object;)D" + insn.owner == srBoxesRunTimeName && (unboxToMethods.get(Type.getReturnType(insn.desc).getSort) match { + case Some((name, desc)) => name == insn.name && desc == insn.desc case _ => false }) } + def getScalaUnbox(primitiveType: Type): MethodInsnNode = { + val (method, desc) = unboxToMethods(primitiveType.getSort) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeName, method, desc, /*itf =*/ false) + } + def isJavaBox(insn: MethodInsnNode): Boolean = { insn.name == "valueOf" && { val args = Type.getArgumentTypes(insn.desc) @@ -392,13 +420,109 @@ object BytecodeUtils { } } - // unused objects created by these constructors are eliminated by pushPop - private val sideEffectFreeConstructors = Set( - "java/lang/Object()V", - "java/lang/String()V", - "java/lang/String(Ljava/lang/String;)V", - "java/lang/String([C)V", + def isJavaUnbox(insn: MethodInsnNode): Boolean = { + insn.desc.startsWith("()") && { + (Type.getReturnType(insn.desc).getSort: @switch) match { + case Type.BOOLEAN => insn.owner == "java/lang/Boolean" && insn.name == "booleanValue" + case Type.BYTE => insn.owner == "java/lang/Byte" && insn.name == "byteValue" + case Type.CHAR => insn.owner == "java/lang/Character" && insn.name == "charValue" + case Type.SHORT => insn.owner == "java/lang/Short" && insn.name == "shortValue" + case Type.INT => insn.owner == "java/lang/Integer" && insn.name == "intValue" + case Type.LONG => insn.owner == "java/lang/Long" && insn.name == "longValue" + case Type.FLOAT => insn.owner == "java/lang/Float" && insn.name == "floatValue" + case Type.DOUBLE => insn.owner == "java/lang/Double" && insn.name == "doubleValue" + case _ => false + } + } + } + + def isPredefAutoBox(insn: MethodInsnNode): Boolean = { + insn.owner == "scala/Predef$" && { + val args = Type.getArgumentTypes(insn.desc) + args.length == 1 && ((args(0).getSort: @switch) match { + case Type.BOOLEAN => insn.name == "boolean2Boolean" && insn.desc == "(Z)Ljava/lang/Boolean;" + case Type.BYTE => insn.name == "byte2Byte" && insn.desc == "(B)Ljava/lang/Byte;" + case Type.CHAR => insn.name == "char2Character" && insn.desc == "(C)Ljava/lang/Character;" + case Type.SHORT => insn.name == "short2Short" && insn.desc == "(S)Ljava/lang/Short;" + case Type.INT => insn.name == "int2Integer" && insn.desc == "(I)Ljava/lang/Integer;" + case Type.LONG => insn.name == "long2Long" && insn.desc == "(J)Ljava/lang/Long;" + case Type.FLOAT => insn.name == "float2Float" && insn.desc == "(F)Ljava/lang/Float;" + case Type.DOUBLE => insn.name == "double2Double" && insn.desc == "(D)Ljava/lang/Double;" + case _ => false + }) + } + } + + def isPredefAutoUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == "scala/Predef$" && { + (Type.getReturnType(insn.desc).getSort: @switch) match { + case Type.BOOLEAN => insn.name == "Boolean2boolean" && insn.desc == "(Ljava/lang/Boolean;)Z" + case Type.BYTE => insn.name == "Byte2byte" && insn.desc == "(Ljava/lang/Byte;)B" + case Type.CHAR => insn.name == "Character2char" && insn.desc == "(Ljava/lang/Character;)C" + case Type.SHORT => insn.name == "Short2short" && insn.desc == "(Ljava/lang/Short;)S" + case Type.INT => insn.name == "Integer2int" && insn.desc == "(Ljava/lang/Integer;)I" + case Type.LONG => insn.name == "Long2long" && insn.desc == "(Ljava/lang/Long;)J" + case Type.FLOAT => insn.name == "Float2float" && insn.desc == "(Ljava/lang/Float;)F" + case Type.DOUBLE => insn.name == "Double2double" && insn.desc == "(Ljava/lang/Double;)D" + case _ => false + } + } + } + + def isRefCreate(insn: MethodInsnNode): Boolean = { + insn.name == "create" && { + val args = Type.getArgumentTypes(insn.desc) + args.length == 1 && ((args(0).getSort: @switch) match { + case Type.BOOLEAN => insn.owner == "scala/runtime/BooleanRef" && insn.desc == "(Z)Lscala/runtime/BooleanRef;" || insn.owner == "scala/runtime/VolatileBooleanRef" && insn.desc == "(Z)Lscala/runtime/VolatileBooleanRef;" + case Type.BYTE => insn.owner == "scala/runtime/ByteRef" && insn.desc == "(B)Lscala/runtime/ByteRef;" || insn.owner == "scala/runtime/VolatileByteRef" && insn.desc == "(B)Lscala/runtime/VolatileByteRef;" + case Type.CHAR => insn.owner == "scala/runtime/CharRef" && insn.desc == "(C)Lscala/runtime/CharRef;" || insn.owner == "scala/runtime/VolatileCharRef" && insn.desc == "(C)Lscala/runtime/VolatileCharRef;" + case Type.SHORT => insn.owner == "scala/runtime/ShortRef" && insn.desc == "(S)Lscala/runtime/ShortRef;" || insn.owner == "scala/runtime/VolatileShortRef" && insn.desc == "(S)Lscala/runtime/VolatileShortRef;" + case Type.INT => insn.owner == "scala/runtime/IntRef" && insn.desc == "(I)Lscala/runtime/IntRef;" || insn.owner == "scala/runtime/VolatileIntRef" && insn.desc == "(I)Lscala/runtime/VolatileIntRef;" + case Type.LONG => insn.owner == "scala/runtime/LongRef" && insn.desc == "(J)Lscala/runtime/LongRef;" || insn.owner == "scala/runtime/VolatileLongRef" && insn.desc == "(J)Lscala/runtime/VolatileLongRef;" + case Type.FLOAT => insn.owner == "scala/runtime/FloatRef" && insn.desc == "(F)Lscala/runtime/FloatRef;" || insn.owner == "scala/runtime/VolatileFloatRef" && insn.desc == "(F)Lscala/runtime/VolatileFloatRef;" + case Type.DOUBLE => insn.owner == "scala/runtime/DoubleRef" && insn.desc == "(D)Lscala/runtime/DoubleRef;" || insn.owner == "scala/runtime/VolatileDoubleRef" && insn.desc == "(D)Lscala/runtime/VolatileDoubleRef;" + case Type.OBJECT => insn.owner == "scala/runtime/ObjectRef" && insn.desc == "(Ljava/lang/Object;)Lscala/runtime/ObjectRef;" || insn.owner == "scala/runtime/VolatileObjectRef" && insn.desc == "(Ljava/lang/Object;)Lscala/runtime/VolatileObjectRef;" + case _ => false + }) + } + } + + private val jlObjectType = Type.getType("Ljava/lang/Object;") + + private val runtimeRefClassesAndTypes = Map( + ("scala/runtime/BooleanRef", Type.BOOLEAN_TYPE), + ("scala/runtime/ByteRef", Type.BYTE_TYPE), + ("scala/runtime/CharRef", Type.CHAR_TYPE), + ("scala/runtime/ShortRef", Type.SHORT_TYPE), + ("scala/runtime/IntRef", Type.INT_TYPE), + ("scala/runtime/LongRef", Type.LONG_TYPE), + ("scala/runtime/FloatRef", Type.FLOAT_TYPE), + ("scala/runtime/DoubleRef", Type.DOUBLE_TYPE), + ("scala/runtime/ObjectRef", jlObjectType), + ("scala/runtime/VolatileBooleanRef", Type.BOOLEAN_TYPE), + ("scala/runtime/VolatileByteRef", Type.BYTE_TYPE), + ("scala/runtime/VolatileCharRef", Type.CHAR_TYPE), + ("scala/runtime/VolatileShortRef", Type.SHORT_TYPE), + ("scala/runtime/VolatileIntRef", Type.INT_TYPE), + ("scala/runtime/VolatileLongRef", Type.LONG_TYPE), + ("scala/runtime/VolatileFloatRef", Type.FLOAT_TYPE), + ("scala/runtime/VolatileDoubleRef", Type.DOUBLE_TYPE), + ("scala/runtime/VolatileObjectRef", jlObjectType)) + + def isRefZero(insn: MethodInsnNode): Boolean = { + insn.name == "zero" && runtimeRefClassesAndTypes.contains(insn.owner) && insn.desc == "()L" + insn.owner + ";" + } + + def runtimeRefClassBoxedType(refClass: InternalName): Type = runtimeRefClassesAndTypes(refClass) + + def isModuleLoad(insn: AbstractInsnNode, moduleName: InternalName): Boolean = insn match { + case fi: FieldInsnNode => fi.getOpcode == GETSTATIC && fi.owner == moduleName && fi.name == "MODULE$" && fi.desc == ("L" + moduleName + ";") + case _ => false + } + def isPredefLoad(insn: AbstractInsnNode) = isModuleLoad(insn, "scala/Predef$") + + private val primitiveBoxConstructors = Set( "java/lang/Boolean(Z)V", "java/lang/Byte(B)V", "java/lang/Character(C)V", @@ -406,8 +530,13 @@ object BytecodeUtils { "java/lang/Integer(I)V", "java/lang/Long(J)V", "java/lang/Float(F)V", - "java/lang/Double(D)V", + "java/lang/Double(D)V") + + def isPrimitiveBoxConstructor(insn: MethodInsnNode): Boolean = { + insn.name == INSTANCE_CONSTRUCTOR_NAME && primitiveBoxConstructors(insn.owner + insn.desc) + } + private val runtimeRefConstructors = Set( "scala/runtime/ObjectRef(Ljava/lang/Object;)V", "scala/runtime/BooleanRef(Z)V", "scala/runtime/ByteRef(B)V", @@ -426,8 +555,13 @@ object BytecodeUtils { "scala/runtime/VolatileIntRef(I)V", "scala/runtime/VolatileLongRef(J)V", "scala/runtime/VolatileFloatRef(F)V", - "scala/runtime/VolatileDoubleRef(D)V" - ) ++ { + "scala/runtime/VolatileDoubleRef(D)V") + + def isRuntimeRefConstructor(insn: MethodInsnNode): Boolean = { + insn.name == INSTANCE_CONSTRUCTOR_NAME && runtimeRefConstructors(insn.owner + insn.desc) + } + + private val tupleConstructors = Set.empty[String] ++ { (1 to 22).map(n => "scala/Tuple" + n + "(" + ("Ljava/lang/Object;" * n) + ")V") } ++ { Iterator("I", "J", "D").map(t => "scala/Tuple1$mc" + t + "$sp(" + t + ")V") @@ -436,6 +570,20 @@ object BytecodeUtils { for (a <- tuple2Specs; b <- tuple2Specs) yield "scala/Tuple2$mc" + a + b + "$sp(" + a + b + ")V" } + def isTupleConstructor(insn: MethodInsnNode): Boolean = { + insn.name == INSTANCE_CONSTRUCTOR_NAME && tupleConstructors(insn.owner + insn.desc) + } + + // unused objects created by these constructors are eliminated by pushPop + private val sideEffectFreeConstructors = primitiveBoxConstructors ++ + runtimeRefConstructors ++ + tupleConstructors ++ Set( + "java/lang/Object()V", + "java/lang/String()V", + "java/lang/String(Ljava/lang/String;)V", + "java/lang/String([C)V" + ) + def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { insn.name == INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors(insn.owner + insn.desc) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 7d2ae60b66..ae5a0954ad 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -34,10 +34,20 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * - empty handlers (try blocks may become empty) * - simplify jumps (goto l; [dead code]; l: ..) => remove goto * - stale local variable descriptors + * - (not box-unbox, which is implemented using prod-cons, so it doesn't consider dead code) * * note that eliminating empty handlers and stale local variable descriptors is required for * correctness, see the comment in the body of `methodOptimizations`. * + * box-unbox elimination (eliminates box-unbox pairs withing the same method) + * + enables UPSTREAM: + * - further box-unbox elimination (e.g. an Integer stored in a Tuple; eliminating the tuple may + * enable eliminating the Integer) + * + enables downstream: + * - copy propagation (new locals are introduced, may be aliases of existing) + * - stale stores (multi-value boxes where not all values are used) + * - empty local variable descriptors (local variables that were holding the box may become unused) + * * copy propagation (replaces LOAD n to the LOAD m for the smallest m that is an alias of n) * + enables downstrem: * - stale stores (a stored value may not be loaded anymore) @@ -51,8 +61,10 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * - push-pop (the new pop may be the single consumer for an instruction) * * push-pop (when a POP is the only consumer of a value, remove the POP and its producer) - * + eanbles UPSTREAM: + * + enables UPSTREAM: * - stale stores (if a LOAD is removed, a corresponding STORE may become stale) + * - box-unbox elimination (push-pop may eliminate a closure allocation, rendering a captured + * box non-escaping) * + enables downstream: * - store-load pairs (a variable may become non-live) * - stale handlers (push-pop removes code) @@ -67,6 +79,7 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * empty handlers (removes exception handlers whose try block is empty) * + enables UPSTREAM: * - unreachable code (catch block becomes unreachable) + * - box-unbox (a box may be escape in an operation in a dead handler) * + enables downstream: * - simplify jumps * @@ -109,6 +122,9 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { import btypes._ import backendUtils._ + val boxUnbox = new BoxUnbox(btypes) + import boxUnbox._ + /** * Remove unreachable code from a method. * @@ -194,57 +210,99 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { /** * Runs the optimizations that depend on each other in a loop until reaching a fixpoint. See * comment in class [[LocalOpt]]. + * + * Returns a pair of booleans (codeChanged, requireEliminateUnusedLocals). */ - def removalRound(runDCE: Boolean, runCopyProp: Boolean, maxRecursion: Int = 10): Boolean = { - if (maxRecursion == 0) return false - - // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores) - // require not having unreachable instructions (null frames). - val dceRequired = runDCE || compilerSettings.YoptCopyPropagation - val (codeRemoved, liveLabels) = if (dceRequired) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) - - // runCopyProp is only true in the first iteration; there's no optimization below that enables - // further copy propagation. it is still part of the fixpoint loop because it needs to run after DCE. - val copyPropChanged = if (runCopyProp) copyProp(method, ownerClassName) else false - - val storesRemoved = if (compilerSettings.YoptCopyPropagation) eliminateStaleStores(method, ownerClassName) else false - - val pushPopRemoved = if (compilerSettings.YoptCopyPropagation) eliminatePushPop(method, ownerClassName) else false - - val storeLoadRemoved = if (compilerSettings.YoptCopyPropagation) eliminateStoreLoad(method) else false - - val removedHandlers = if (dceRequired) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] + def removalRound( + requestDCE: Boolean, + requestBoxUnbox: Boolean, + requestStaleStores: Boolean, + requestStoreLoad: Boolean, + firstIteration: Boolean, + maxRecursion: Int = 10): (Boolean, Boolean) = { + if (maxRecursion == 0) return (false, false) + + // UNREACHABLE CODE + // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores, + // boxUnboxElimination) require not having unreachable instructions (null frames). + val runDCE = (compilerSettings.YoptUnreachableCode && requestDCE) || + compilerSettings.YoptBoxUnbox || + compilerSettings.YoptCopyPropagation + val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) + + // BOX-UNBOX + val runBoxUnbox = compilerSettings.YoptBoxUnbox && requestBoxUnbox + val boxUnboxChanged = runBoxUnbox && boxUnboxElimination(method, ownerClassName) + + // COPY PROPAGATION + val runCopyProp = compilerSettings.YoptCopyPropagation && (firstIteration || boxUnboxChanged) + val copyPropChanged = runCopyProp && copyProp(method, ownerClassName) + + // STALE STORES + val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || codeRemoved || boxUnboxChanged || copyPropChanged) + val storesRemoved = runStaleStores && eliminateStaleStores(method, ownerClassName) + + // PUSH-POP + val runPushPop = compilerSettings.YoptCopyPropagation && (firstIteration || storesRemoved) + val pushPopRemoved = runPushPop && eliminatePushPop(method, ownerClassName) + + // STORE-LOAD PAIRS + val runStoreLoad = compilerSettings.YoptCopyPropagation && (requestStoreLoad || boxUnboxChanged || copyPropChanged || pushPopRemoved) + val storeLoadRemoved = runStoreLoad && eliminateStoreLoad(method) + + // STALE HANDLERS + val removedHandlers = if (runDCE) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] val handlersRemoved = removedHandlers.nonEmpty val liveHandlerRemoved = removedHandlers.exists(h => liveLabels(h.start)) - val jumpsChanged = if (compilerSettings.YoptSimplifyJumps) simplifyJumps(method) else false + // SIMPLIFY JUMPS + // almost all of the above optimizations enable simplifying more jumps, so we just run it in every iteration + val runSimplifyJumps = compilerSettings.YoptSimplifyJumps + val jumpsChanged = runSimplifyJumps && simplifyJumps(method) // See doc comment in the beginning of this file (optimizations marked UPSTREAM) - if (liveHandlerRemoved || jumpsChanged) { - // changing live handlers or jumps enables DCE to remove more code, so runDCE = true - removalRound(runDCE = true, runCopyProp = false, maxRecursion = maxRecursion - 1) - } else if (pushPopRemoved) { - // pushPop doesn't enable DCE, but other optimizations (stale stores). so we iterate, but without DCE. - removalRound(runDCE = false, runCopyProp = false, maxRecursion = maxRecursion - 1) - } - - codeRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged + val runDCEAgain = liveHandlerRemoved || jumpsChanged + val runBoxUnboxAgain = boxUnboxChanged || pushPopRemoved || liveHandlerRemoved + val runStaleStoresAgain = pushPopRemoved + val runStoreLoadAgain = jumpsChanged + val runAgain = runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runStoreLoadAgain + + val downstreamRequireEliminateUnusedLocals = runAgain && removalRound( + requestDCE = runDCEAgain, + requestBoxUnbox = runBoxUnboxAgain, + requestStaleStores = runStaleStoresAgain, + requestStoreLoad = runStoreLoadAgain, + firstIteration = false, + maxRecursion = maxRecursion - 1)._2 + + val requireEliminateUnusedLocals = downstreamRequireEliminateUnusedLocals || + codeRemoved || // see comment in method `methodOptimizations` + boxUnboxChanged || // box-unbox renders locals (holding boxes) unused + storesRemoved || + storeLoadRemoved || + handlersRemoved + + val codeChanged = codeRemoved || boxUnboxChanged || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged + (codeChanged, requireEliminateUnusedLocals) } - val dceCopyPropHandlersOrJumpsChanged = if (AsmAnalyzer.sizeOKForBasicValue(method)) { + val (dceBoxesCopypropPushpopOrJumpsChanged, requireEliminateUnusedLocals) = if (AsmAnalyzer.sizeOKForBasicValue(method)) { // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE // here is more thorough than `minimalRemoveUnreachableCode` that run before inlining. val r = removalRound( - runDCE = compilerSettings.YoptUnreachableCode, - runCopyProp = compilerSettings.YoptCopyPropagation) + requestDCE = true, + requestBoxUnbox = true, + requestStaleStores = true, + requestStoreLoad = true, + firstIteration = true) if (compilerSettings.YoptUnreachableCode) unreachableCodeEliminated += method r - } else false + } else (false, false) - // (*) Removing stale local variable descriptors is required for correctness of unreachable-code + // (*) Removing stale local variable descriptors is required for correctness, see comment in `methodOptimizations` val localsRemoved = if (compilerSettings.YoptCompactLocals) compactLocalVariables(method) // also removes unused - else if (compilerSettings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*) + else if (requireEliminateUnusedLocals) removeUnusedLocalVariableNodes(method)() // (*) else false val lineNumbersRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLineNumbers(method) else false @@ -257,7 +315,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations) assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) - dceCopyPropHandlersOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + dceBoxesCopypropPushpopOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved } /** @@ -602,8 +660,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val ProducedValue(prod, size) = queue.dequeue() def prodString = s"Producer ${AsmUtils textify prod}@${method.instructions.indexOf(prod)}\n${AsmUtils textify method}" - def popAfterProd(): Unit = { - toInsertAfter(prod) = getPop(size) } + def popAfterProd(): Unit = toInsertAfter(prod) = getPop(size) (prod.getOpcode: @switch) match { case ACONST_NULL | ICONST_M1 | ICONST_0 | ICONST_1 | ICONST_2 | ICONST_3 | ICONST_4 | ICONST_5 | LCONST_0 | LCONST_1 | FCONST_0 | FCONST_1 | FCONST_2 | DCONST_0 | DCONST_1 | @@ -629,7 +686,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { case IADD | LADD | FADD | DADD | ISUB | LSUB | FSUB | DSUB | IMUL | LMUL | FMUL | DMUL | FDIV | DDIV | FREM | DREM | LSHL | LSHR | LUSHR | IAND | IOR | IXOR | LAND | LOR | LXOR | - LCMP | FCMPL | FCMPG | DCMPL | DCMPG=> + LCMP | FCMPL | FCMPG | DCMPL | DCMPG => toRemove += prod handleInputs(prod, 2) @@ -753,7 +810,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // vice-versa, eliminating a constructor call adds producers of constructor parameters to the queue. // so the two run in a loop. runQueue() - while(eliminateUnusedPureConstructorCalls()) + while (eliminateUnusedPureConstructorCalls()) runQueue() var changed = false @@ -976,7 +1033,7 @@ object LocalOptImpls { def removeEmptyExceptionHandlers(method: MethodNode): Set[TryCatchBlockNode] = { /** True if there exists code between start and end. */ def containsExecutableCode(start: AbstractInsnNode, end: LabelNode): Boolean = { - start != end && ((start.getOpcode : @switch) match { + start != end && ((start.getOpcode: @switch) match { // FrameNode, LabelNode and LineNumberNode have opcode == -1. case -1 | GOTO => containsExecutableCode(start.getNext, end) case _ => true @@ -985,7 +1042,7 @@ object LocalOptImpls { var removedHandlers = Set.empty[TryCatchBlockNode] val handlersIter = method.tryCatchBlocks.iterator() - while(handlersIter.hasNext) { + while (handlersIter.hasNext) { val handler = handlersIter.next() if (!containsExecutableCode(handler.start, handler.end)) { removedHandlers += handler @@ -1004,9 +1061,10 @@ object LocalOptImpls { * same type or name. */ def removeUnusedLocalVariableNodes(method: MethodNode)(firstLocalIndex: Int = parametersSize(method), renumber: Int => Int = identity): Boolean = { - def variableIsUsed(start: AbstractInsnNode, end: LabelNode, varIndex: Int): Boolean = { + @tailrec def variableIsUsed(start: AbstractInsnNode, end: LabelNode, varIndex: Int): Boolean = { start != end && (start match { case v: VarInsnNode if v.`var` == varIndex => true + case i: IincInsnNode if i.`var` == varIndex => true case _ => variableIsUsed(start.getNext, end, varIndex) }) } diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 794c0ba324..42ed9ef935 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -229,6 +229,7 @@ trait ScalaSettings extends AbsScalaSettings val simplifyJumps = Choice("simplify-jumps", "Simplify branching instructions, eliminate unnecessary ones.") val compactLocals = Choice("compact-locals", "Eliminate empty slots in the sequence of local variables.") val copyPropagation = Choice("copy-propagation", "Eliminate redundant local variables and unused values (including closures). Enables unreachable-code.") + val boxUnbox = Choice("box-unbox", "Eliminate box-unbox pairs within the same method (also tuples, xRefs, value class instances). Enables unreachable-code.") val nullnessTracking = Choice("nullness-tracking", "Track nullness / non-nullness of local variables and apply optimizations.") val closureInvocations = Choice("closure-invocations" , "Rewrite closure invocations to the implementation method.") val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled. Enables unreachable-code.") @@ -239,7 +240,7 @@ trait ScalaSettings extends AbsScalaSettings private val defaultChoices = List(unreachableCode) val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices) - private val methodChoices = List(unreachableCode, simplifyJumps, compactLocals, copyPropagation, nullnessTracking, closureInvocations) + private val methodChoices = List(unreachableCode, simplifyJumps, compactLocals, copyPropagation, boxUnbox, nullnessTracking, closureInvocations) val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) private val projectChoices = List(lMethod, inlineProject) @@ -260,6 +261,7 @@ trait ScalaSettings extends AbsScalaSettings def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps) def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals) def YoptCopyPropagation = Yopt.contains(YoptChoices.copyPropagation) + def YoptBoxUnbox = Yopt.contains(YoptChoices.boxUnbox) def YoptNullnessTracking = Yopt.contains(YoptChoices.nullnessTracking) def YoptClosureInvocations = Yopt.contains(YoptChoices.closureInvocations) diff --git a/src/partest-extras/scala/tools/partest/ASMConverters.scala b/src/partest-extras/scala/tools/partest/ASMConverters.scala index b4c686473b..d990160ce8 100644 --- a/src/partest-extras/scala/tools/partest/ASMConverters.scala +++ b/src/partest-extras/scala/tools/partest/ASMConverters.scala @@ -38,6 +38,11 @@ object ASMConverters { } def dropNonOp = dropLinesFrames.dropStaleLabels + + def summary: List[Any] = dropNonOp map { + case i: Invoke => i.name + case i => i.opcode + } } sealed abstract class Instruction extends Product { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index bf10b88a24..7b3936f867 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -1313,88 +1313,128 @@ class InlinerTest extends ClearAfterClass { """.stripMargin val List(c, _, _) = compile(code) - def instructionSummary(m: Method): List[Any] = m.instructions.dropNonOp map { - case i: Invoke => i.name - case i => i.opcode - } - - assertEquals(instructionSummary(getSingleMethod(c, "t1")), + assertEquals(getSingleMethod(c, "t1").instructions.summary, List(BIPUSH, "C$$$anonfun$1", IRETURN)) - assertEquals(instructionSummary(getSingleMethod(c, "t1a")), + assertEquals(getSingleMethod(c, "t1a").instructions.summary, List(LCONST_1, "C$$$anonfun$2", IRETURN)) - assertEquals(instructionSummary(getSingleMethod(c, "t2")), List( - ICONST_1, "boxToByte", - ICONST_2, "boxToInteger", ASTORE, - "unboxToByte", - ALOAD, "unboxToInt", - "C$$$anonfun$3", - "boxToInteger", "unboxToInt", IRETURN)) + assertEquals(getSingleMethod(c, "t2").instructions.summary, List( + ICONST_1, ICONST_2, "C$$$anonfun$3",IRETURN)) // val a = new ValKl(n); new ValKl(anonfun(a.x)).x // value class instantiation-extraction should be optimized by boxing elim - assertEquals(instructionSummary(getSingleMethod(c, "t3")), List( + assertEquals(getSingleMethod(c, "t3").instructions.summary, List( NEW, DUP, ICONST_1, "", ASTORE, NEW, DUP, ALOAD, CHECKCAST, "x", "C$$$anonfun$4", "", CHECKCAST, "x", IRETURN)) - assertEquals(instructionSummary(getSingleMethod(c, "t4")), List( - BIPUSH, "boxToInteger", "unboxToInt", - "C$$$anonfun$5", - "boxToInteger", ARETURN)) - - assertEquals(instructionSummary(getSingleMethod(c, "t4a")), List( - ICONST_1, "boxToInteger", - LDC, "boxToDouble", "unboxToDouble", DSTORE, - "unboxToInt", DLOAD, "C$$$anonfun$6", - "boxToLong", "unboxToLong", LRETURN)) - - assertEquals(instructionSummary(getSingleMethod(c, "t5")), List( - BIPUSH, "boxToInteger", - ICONST_3, "boxToByte", ASTORE, - "unboxToInt", ALOAD, - "unboxToByte", - "C$$$anonfun$7", - "boxToInteger", ARETURN)) - - assertEquals(instructionSummary(getSingleMethod(c, "t5a")), List( - BIPUSH, "boxToInteger", - BIPUSH, I2B, "boxToByte", ASTORE, - "unboxToInt", ALOAD, - "unboxToByte", - "C$$$anonfun$8", - "boxToInteger", "unboxToInt", IRETURN)) - - assertEquals(instructionSummary(getSingleMethod(c, "t6")), List( + assertEquals(getSingleMethod(c, "t4").instructions.summary, List( + BIPUSH, "C$$$anonfun$5", "boxToInteger", ARETURN)) + + assertEquals(getSingleMethod(c, "t4a").instructions.summary, List( + ICONST_1, LDC, "C$$$anonfun$6", LRETURN)) + + assertEquals(getSingleMethod(c, "t5").instructions.summary, List( + BIPUSH, ICONST_3, "C$$$anonfun$7", "boxToInteger", ARETURN)) + + assertEquals(getSingleMethod(c, "t5a").instructions.summary, List( + BIPUSH, BIPUSH, I2B, "C$$$anonfun$8", IRETURN)) + + assertEquals(getSingleMethod(c, "t6").instructions.summary, List( BIPUSH, "C$$$anonfun$9", RETURN)) - assertEquals(instructionSummary(getSingleMethod(c, "t7")), List( - ICONST_1, "boxToBoolean", "unboxToBoolean", - "C$$$anonfun$10", RETURN)) + assertEquals(getSingleMethod(c, "t7").instructions.summary, List( + ICONST_1, "C$$$anonfun$10", RETURN)) - assertEquals(instructionSummary(getSingleMethod(c, "t8")), List( - ICONST_1, "boxToInteger", - LDC, "boxToDouble", "unboxToDouble", DSTORE, - "unboxToInt", DLOAD, "C$$$anonfun$11", - "boxToLong", "unboxToLong", LRETURN)) + assertEquals(getSingleMethod(c, "t8").instructions.summary, List( + ICONST_1, LDC, "C$$$anonfun$11", LRETURN)) - assertEquals(instructionSummary(getSingleMethod(c, "t9")), List( + assertEquals(getSingleMethod(c, "t9").instructions.summary, List( ICONST_1, "boxToInteger", "C$$$anonfun$12", RETURN)) // t9a inlines Range.foreach, which is quite a bit of code, so just testing the core assertInvoke(getSingleMethod(c, "t9a"), "C", "C$$$anonfun$13") - assert(instructionSummary(getSingleMethod(c, "t9a")).contains("boxToInteger")) + assert(getSingleMethod(c, "t9a").instructions.summary.contains("boxToInteger")) - assertEquals(instructionSummary(getSingleMethod(c, "t10")), List( + assertEquals(getSingleMethod(c, "t10").instructions.summary, List( ICONST_1, ISTORE, ALOAD, ILOAD, "C$$$anonfun$14", RETURN)) // t10a inlines Range.foreach assertInvoke(getSingleMethod(c, "t10a"), "C", "C$$$anonfun$15") - assert(!instructionSummary(getSingleMethod(c, "t10a")).contains("boxToInteger")) + assert(!getSingleMethod(c, "t10a").instructions.summary.contains("boxToInteger")) + } + + @Test + def refElimination(): Unit = { + val code = + """class C { + | def t1 = { + | var i = 0 + | @inline def inner() = i += 1 + | inner() + | i + | } + | + | final def m(f: Int => Unit) = f(10) + | def t2 = { + | var x = -1 // IntRef not yet eliminated: closure elimination does not + | m(i => if (i == 10) x = 1) // yet inline the anonfun method, need to improve the heuristsics + | x + | } + |} + """.stripMargin + val List(c) = compile(code) + assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(Op(ICONST_0), Op(ICONST_1), Op(IADD), Op(IRETURN))) + assertEquals(getSingleMethod(c, "t2").instructions collect { case i: Invoke => i.owner +"."+ i.name }, List( + "scala/runtime/IntRef.create", "C.C$$$anonfun$1")) + } + + @Test + def tupleElimination(): Unit = { + val code = + """class C { + | @inline final def tpl[A, B](a: A, b: B) = (a, b) + | @inline final def t_1[A, B](t: (A, B)) = t._1 + | @inline final def t_2[A, B](t: (A, B)) = t._2 + | + | def t1 = { + | val t = (3, 4) // specialized tuple + | t_1(t) + t_2(t) // invocations to generic _1 / _2, box operation inserted when eliminated + | } + | + | def t2 = { + | val t = tpl(1, 2) // generic Tuple2[Integer, Integer] created + | t._1 + t._2 // invokes the specialized _1$mcI$sp, eliminating requires adding an unbox operation + | } + | + | @inline final def m = (1, 3) + | def t3 = { + | val (a, b) = m + | a - b + | } + | + | def t4 = { + | val ((a, b), (c, d)) = (m, m) + | a + b + c + d + | } + | + | def t5 = m match { + | case (1, y) => y + | case (x, y) => x * y + | } + |} + """.stripMargin + val List(c) = compile(code) + assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(Op(ICONST_3), Op(ICONST_4), Op(IADD), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(Op(ICONST_1), Op(ICONST_2), Op(IADD), Op(IRETURN))) + // tuple not yet eliminated due to null checks, casts + assert(getSingleMethod(c, "t3").instructions.exists(_.opcode == IFNONNULL)) + assert(getSingleMethod(c, "t4").instructions.exists(_.opcode == CHECKCAST)) + assert(getSingleMethod(c, "t5").instructions.exists(_.opcode == IFNULL)) } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala index f4524f0bb1..94c9f55728 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -252,4 +252,241 @@ class MethodLevelOptsTest extends ClearAfterClass { val t = getSingleMethod(c, "t") assert(!t.instructions.exists(_.opcode == INVOKEDYNAMIC), t) } + + @Test + def boxUnboxPrimitive(): Unit = { + val code = + """class C { + | def t1 = { + | val a: Any = runtime.BoxesRunTime.boxToInteger(1) + | runtime.BoxesRunTime.unboxToInt(a) + 1 + | } + | + | // two box and two unbox operations + | def t2(b: Boolean) = { + | val a = if (b) (3l: Any) else 2l + | a.asInstanceOf[Long] + 1 + a.asInstanceOf[Long] + | } + | + | def t3(i: Integer): Int = i.asInstanceOf[Int] + | + | def t4(l: Long): Any = l + | + | def t5(i: Int): Int = { + | val b = Integer.valueOf(i) + | val c: Integer = i + | b.asInstanceOf[Int] + c.intValue + | } + | + | def t6: Long = { + | val y = new java.lang.Boolean(true) + | val i: Integer = if (y) new Integer(10) else 13 + | val j: java.lang.Long = 3l + | j + i + | } + | + | def t7: Int = { + | val a: Any = 3 + | a.asInstanceOf[Int] + a.asInstanceOf[Int] + | } + | + | def t8 = null.asInstanceOf[Int] + | + | def t9: Int = { + | val a = Integer.valueOf(10) + | val b = runtime.BoxesRunTime.unboxToInt(a) + | a + b + | } + | + | @noinline def escape(a: Any) = () + | + | // example E4 in BoxUnbox doc comment + | def t10: Int = { + | val a = Integer.valueOf(10) // int 10 is stored into local + | escape(a) + | a // no unbox, 10 is read from local + | } + | + | // the boxes here cannot be eliminated. see doc comment in BoxUnbox, example E1. + | def t11(b: Boolean): Int = { + | val i = Integer.valueOf(10) + | val j = Integer.valueOf(41) + | escape(i) // force rewrite method M1 (see doc in BoxUnbox) + | val res: Integer = if (b) i else j + | res.toInt // cannot be re-written to a local variable read - we don't know which local to read + | } + | + | // both boxes have a single unboxing consumer, and the escape. note that the escape does + | // NOT put the two boxes into the same set of rewrite operations: we can rewrite both + | // boxes with their unbox individually. in both cases the box also escapes, so method + | // M1 will keep the box around. + | def t12(b: Boolean): Int = { + | val i = Integer.valueOf(10) + | val j = Integer.valueOf(32) + | escape(if (b) i else j) // force method M1. the escape here is a consumer for both boxes + | if (b) i.toInt else j.toInt // both boxes (i, j) have their own unboxing consumer + | } + |} + """.stripMargin + + val List(c) = compileClasses(methodOptCompiler)(code) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + assertInvoke(getSingleMethod(c, "t3"), "scala/runtime/BoxesRunTime", "unboxToInt") + assertInvoke(getSingleMethod(c, "t4"), "scala/runtime/BoxesRunTime", "boxToLong") + assertNoInvoke(getSingleMethod(c, "t5")) + assertNoInvoke(getSingleMethod(c, "t6")) + assertNoInvoke(getSingleMethod(c, "t7")) + assertEquals(getSingleMethod(c, "t8").instructions.summary, List(ACONST_NULL, "unboxToInt", IRETURN)) + assertNoInvoke(getSingleMethod(c, "t9")) + // t10: no invocation of unbox + assertEquals(getSingleMethod(c, "t10").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("java/lang/Integer", "valueOf"), + ("C", "escape"))) + + assertEquals(getSingleMethod(c, "t11").instructions.summary, List( + BIPUSH, "valueOf", ASTORE /*2*/, + BIPUSH, "valueOf", ASTORE /*3*/, + ALOAD /*0*/, ALOAD /*2*/, "escape", + ILOAD /*1*/, IFEQ /*L1*/, ALOAD /*2*/, GOTO /*L2*/, /*Label L1*/ -1, ALOAD /*3*/, /*Label L2*/ -1, + ASTORE /*4*/, GETSTATIC /*Predef*/, ALOAD /*4*/, "Integer2int", IRETURN)) + + // no unbox invocations + assertEquals(getSingleMethod(c, "t12").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("java/lang/Integer", "valueOf"), + ("java/lang/Integer", "valueOf"), + ("C", "escape"))) + } + + @Test + def refEliminiation(): Unit = { + val code = + """class C { + | import runtime._ + | @noinline def escape(a: Any) = () + | + | def t1 = { // box eliminated + | val r = new IntRef(0) + | r.elem + | } + | + | def t2(b: Boolean) = { + | val r1 = IntRef.zero() // both eliminated + | val r2 = IntRef.create(1) + | val res: IntRef = if (b) r1 else r2 + | res.elem + | } + | + | def t3 = { + | val r = LongRef.create(10l) // eliminated + | r.elem += 3 + | r.elem + | } + | + | def t4(b: Boolean) = { + | val x = BooleanRef.create(false) // eliminated + | if (b) x.elem = true + | if (x.elem) "a" else "b" + | } + | + | def t5 = { + | val r = IntRef.create(10) // not eliminated: the box might be modified in the escape + | escape(r) + | r.elem + | } + | + | def t6(b: Boolean) = { + | val r1 = IntRef.zero() + | val r2 = IntRef.create(1) + | r1.elem = 39 + | val res: IntRef = if (b) r1 else r2 + | res.elem // boxes remain: can't rewrite this read, don't know which local + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t1").instructions.summary, List(ICONST_0, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t2")) + assertEquals(getSingleMethod(c, "t3").instructions.summary, List(LDC, LDC, LADD, LRETURN)) + assertNoInvoke(getSingleMethod(c, "t4")) + assertEquals(getSingleMethod(c, "t5").instructions collect { case Field(_, owner, name, _) => s"$owner.$name" }, + List("scala/runtime/IntRef.elem")) + assertEquals(getSingleMethod(c, "t6").instructions collect { case Field(op, owner, name, _) => s"$op $owner.$name" }, + List(s"$PUTFIELD scala/runtime/IntRef.elem", s"$GETFIELD scala/runtime/IntRef.elem")) + } + + @Test + def tupleElimination(): Unit = { + val code = + """class C { + | def t1(b: Boolean) = { + | val t = ("hi", "fish") + | if (b) t._1 else t._2 + | } + | + | def t2 = { + | val t = (1, 3) // specialized tuple + | t._1 + t._2 // specialized accessors (_1$mcII$sp) + | } + | + | def t3 = { + | // boxed before tuple creation, a non-specialized tuple is created + | val t = (new Integer(3), Integer.valueOf(4)) + | t._1 + t._2 // invokes the generic `_1` / `_2` getters, both values unboxed by Integer2int + | } + | + | def t4: Any = { + | val t = (3, 3) // specialized tuple is created, ints are not boxed + | (t: Tuple2[Any, Any])._1 // when eliminating the _1 call, need to insert a boxing operation + | } + | + | // the inverse of t4 also happens: an Tuple[Integer] where _1$mcI$sp is invoked. In this + | // case, an unbox operation needs to be added when eliminating the extraction. The only + | // way I found to test this is with an inlined generic method, see InlinerTest.tupleElimination. + | def tpl[A, B](a: A, b: B) = (a, b) + | def t5: Int = tpl(1, 2)._1 // invokes _1$mcI$sp + | + | def t6 = { + | val (a, b) = (1, 2) + | a - b + | } + | + | def t7 = { + | // this example is more tricky to handle than it looks, see doc comment in BoxUnbox. + | val ((a, b), c) = ((1, 2), 3) + | a + b + c + | } + | + | def t8 = { + | val ((a, b), (c, d)) = ((1, 2), (3, Integer.valueOf(10))) + | a + b + c + d + | } + | + | def t9(a: Int, b: Int) = (a, b) match { // tuple is optimized away + | case (x, y) if x == y => 0 + | case (x, y) => x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertEquals(getSingleMethod(c, "t2").instructions.summary, List(ICONST_1, ICONST_3, IADD, IRETURN)) + // cannot eliminate boxes because of casts + assertEquals(getSingleMethod(c, "t3").instructions collect { case TypeOp(CHECKCAST, tp) => tp }, List("java/lang/Integer", "java/lang/Integer")) + assertEquals(getSingleMethod(c, "t4").instructions.summary, List(ICONST_3, "boxToInteger", ARETURN)) + assertEquals(getSingleMethod(c, "t5").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("scala/runtime/BoxesRunTime", "boxToInteger"), + ("scala/runtime/BoxesRunTime", "boxToInteger"), + ("C", "tpl"), + ("scala/Tuple2", "_1$mcI$sp"))) + // cannot eliminate boxes because of null checks + assert(getSingleMethod(c, "t6").instructions.exists(_.opcode == IFNONNULL)) + // cannot eliminate boxed because of casts and null checks + def castsNullChecks(m: String) = getSingleMethod(c, m).instructions collect { case op if op.opcode == IFNULL || op.opcode == CHECKCAST => op } + assertEquals(castsNullChecks("t7"), + List(Jump(IFNULL, Label(29)), TypeOp(CHECKCAST, "scala/Tuple2"), Jump(IFNULL, Label(29)))) + assertEquals(castsNullChecks("t8").size, 7) + assertEquals(castsNullChecks("t9").size, 2) + } } -- cgit v1.2.3 From e4319789ea6d1a4e958d383619cf7e51338bfde2 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Tue, 15 Dec 2015 15:03:08 +0100 Subject: Eliminate unnecessary casts Eliminate casts that are statically known to succeed. This enables boxes to be eliminated and simplifies the implementation of closure allocation elimination. --- .../nsc/backend/jvm/analysis/BackendUtils.scala | 2 + .../backend/jvm/analysis/TypeFlowInterpreter.scala | 36 +++++++++ .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 89 ++++++++++++++-------- .../scala/tools/nsc/settings/ScalaSettings.scala | 4 +- .../nsc/backend/jvm/opt/ClosureOptimizerTest.scala | 2 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 40 +++++++++- .../nsc/backend/jvm/opt/MethodLevelOptsTest.scala | 9 +-- 7 files changed, 138 insertions(+), 44 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/backend/jvm/analysis/TypeFlowInterpreter.scala diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala index e4fdcdc542..739b775e56 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala @@ -57,6 +57,8 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { class ProdConsAnalyzer(val methodNode: MethodNode, classInternalName: InternalName) extends AsmAnalyzer(methodNode, classInternalName, new Analyzer(new InitialProducerSourceInterpreter)) with ProdConsAnalyzerImpl + class NonLubbingTypeFlowAnalyzer(val methodNode: MethodNode, classInternalName: InternalName) extends AsmAnalyzer(methodNode, classInternalName, new Analyzer(new NonLubbingTypeFlowInterpreter)) + /** * Add: * private static java.util.Map $deserializeLambdaCache$ = null diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/TypeFlowInterpreter.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/TypeFlowInterpreter.scala new file mode 100644 index 0000000000..bcf9978c16 --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/TypeFlowInterpreter.scala @@ -0,0 +1,36 @@ +package scala.tools.nsc +package backend.jvm +package analysis + +import scala.tools.asm.Type +import scala.tools.asm.tree.analysis.{BasicValue, BasicInterpreter} + +abstract class TypeFlowInterpreter extends BasicInterpreter { + override def newValue(tp: Type) = { + if (tp == null) super.newValue(tp) + else if (isRef(tp)) new BasicValue(tp) + else super.newValue(tp) + } + + def isRef(tp: Type) = tp != null && (tp.getSort match { + case Type.OBJECT | Type.ARRAY => true + case _ => false + }) + + def refLub(a: BasicValue, b: BasicValue): BasicValue + + override def merge(a: BasicValue, b: BasicValue): BasicValue = { + if (a == b) a + else if (isRef(a.getType) && isRef(b.getType)) refLub(a, b) + else BasicValue.UNINITIALIZED_VALUE + } +} + +/** + * A [[TypeFlowInterpreter]] which collapses LUBs of non-equal reference types to Object. + * This could be made more precise by looking up ClassBTypes for the two reference types and using + * the `jvmWiseLUB` method. + */ +class NonLubbingTypeFlowInterpreter extends TypeFlowInterpreter { + def refLub(a: BasicValue, b: BasicValue): BasicValue = BasicValue.REFERENCE_VALUE // java/lang/Object +} diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index ae5a0954ad..3b03a12405 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -46,6 +46,8 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * + enables downstream: * - copy propagation (new locals are introduced, may be aliases of existing) * - stale stores (multi-value boxes where not all values are used) + * - redundant casts (`("a", "b")._1`: the generic `_1` method returns `Object`, a cast + * to String is added. The cast is redundant after eliminating the tuple.) * - empty local variable descriptors (local variables that were holding the box may become unused) * * copy propagation (replaces LOAD n to the LOAD m for the smallest m that is an alias of n) @@ -60,6 +62,12 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * + enables downstream: * - push-pop (the new pop may be the single consumer for an instruction) * + * redundant casts: eliminates casts that are statically known to succeed (uses type propagation) + * + enables UPSTREAM: + * - box-unbox elimination (a removed checkcast may be a box consumer) + * + enables downstream: + * - push-pop for closure allocation elimination (every indyLambda is followed by a checkcast, see SI-9540) + * * push-pop (when a POP is the only consumer of a value, remove the POP and its producer) * + enables UPSTREAM: * - stale stores (if a LOAD is removed, a corresponding STORE may become stale) @@ -120,6 +128,7 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ class LocalOpt[BT <: BTypes](val btypes: BT) { import LocalOptImpls._ import btypes._ + import coreBTypes._ import backendUtils._ val boxUnbox = new BoxUnbox(btypes) @@ -242,8 +251,12 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || codeRemoved || boxUnboxChanged || copyPropChanged) val storesRemoved = runStaleStores && eliminateStaleStores(method, ownerClassName) + // REDUNDANT CASTS + val runRedundantCasts = compilerSettings.YoptRedundantCasts && (firstIteration || boxUnboxChanged) + val castRemoved = runRedundantCasts && eliminateRedundantCasts(method, ownerClassName) + // PUSH-POP - val runPushPop = compilerSettings.YoptCopyPropagation && (firstIteration || storesRemoved) + val runPushPop = compilerSettings.YoptCopyPropagation && (firstIteration || storesRemoved || castRemoved) val pushPopRemoved = runPushPop && eliminatePushPop(method, ownerClassName) // STORE-LOAD PAIRS @@ -262,7 +275,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // See doc comment in the beginning of this file (optimizations marked UPSTREAM) val runDCEAgain = liveHandlerRemoved || jumpsChanged - val runBoxUnboxAgain = boxUnboxChanged || pushPopRemoved || liveHandlerRemoved + val runBoxUnboxAgain = boxUnboxChanged || castRemoved || pushPopRemoved || liveHandlerRemoved val runStaleStoresAgain = pushPopRemoved val runStoreLoadAgain = jumpsChanged val runAgain = runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runStoreLoadAgain @@ -282,7 +295,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { storeLoadRemoved || handlersRemoved - val codeChanged = codeRemoved || boxUnboxChanged || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged + val codeChanged = codeRemoved || boxUnboxChanged || castRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged (codeChanged, requireEliminateUnusedLocals) } @@ -716,36 +729,6 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { case _ => popAfterProd() } - case CHECKCAST => - // Special case for `IndyLambda JFunctionN; CHECKCAST FuncionN`. This sequence is emitted - // for all closure instantiations, see https://issues.scala-lang.org/browse/SI-9540 - val castType = prod.asInstanceOf[TypeInsnNode].desc - if (isScalaFunctionType(castType)) { - - // the `FunctionN` part of an internal name (e.g. `scala/runtime/java8/JFunction1$mcDI$sp`) - def funPart(funType: String): String = { - var end = funType.indexOf('$') - if (end == -1) end = funType.length - funType.substring(funType.lastIndexOf('/') + 1, end) - } - - // true if the cast will always succeed - def castOk(indy: InvokeDynamicInsnNode): Boolean = { - val generatedFunctionType = Type.getReturnType(indy.desc).getInternalName - isrJFunctionType(generatedFunctionType) && funPart(generatedFunctionType) == "J" + funPart(castType) - } - - val castProd = producersIfSingleConsumer(prod, prodCons.frameAt(prod).stackTop) - if (castProd.size == 1) castProd.head match { - case callGraph.LambdaMetaFactoryCall(indy, _, _, _) if castOk(indy) => - toRemove += prod // remove the cast - handleClosureInst(indy) // remove the indyLambda - case _ => popAfterProd() - } else - popAfterProd() - } else - popAfterProd() - case NEW => if (isNewForSideEffectFreeConstructor(prod)) toRemove += prod else popAfterProd() @@ -847,6 +830,46 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { override def toString = s"<${AsmUtils textify producer}>" } + /** + * Eliminate `CHECKCAST` instructions that are statically known to succeed. This is safe if the + * tested object is null: `null.asInstanceOf` always succeeds. + * + * The type of the tested object is determined using a NonLubbingTypeFlowAnalyzer. Note that this + * analysis collapses LUBs of non-equal references types to Object for simplicity. Example: + * given `B <: A <: Object`, the cast in `(if (..) new B else new A).asInstanceOf[A]` would not + * be eliminated. + * + * Note: we cannot replace `INSTANCEOF` tests by only looking at the types, `null.isInstanceOf` + * always returns false, so we'd also need nullness information. + */ + def eliminateRedundantCasts(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForBasicValue(method) && { + def isSubType(aRefDesc: String, bClass: InternalName): Boolean = aRefDesc == bClass || bClass == ObjectRef.internalName || { + (bTypeForDescriptorOrInternalNameFromClassfile(aRefDesc) conformsTo classBTypeFromParsedClassfile(bClass)).getOrElse(false) + } + + lazy val typeAnalyzer = new NonLubbingTypeFlowAnalyzer(method, owner) + + // cannot remove instructions while iterating, it gets the analysis out of synch (indexed by instructions) + val toRemove = mutable.Set.empty[TypeInsnNode] + + val it = method.instructions.iterator() + while (it.hasNext) it.next() match { + case ti: TypeInsnNode if ti.getOpcode == CHECKCAST => + val frame = typeAnalyzer.frameAt(ti) + val valueTp = frame.getValue(frame.stackTop) + if (valueTp.isReference && isSubType(valueTp.getType.getDescriptor, ti.desc)) { + toRemove += ti + } + + case _ => + } + + toRemove foreach method.instructions.remove + toRemove.nonEmpty + } + } + /** * Remove `xSTORE n; xLOAD n` paris if * - the local variable n is not used anywhere else in the method (1), and diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 42ed9ef935..87a9c262f4 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -229,6 +229,7 @@ trait ScalaSettings extends AbsScalaSettings val simplifyJumps = Choice("simplify-jumps", "Simplify branching instructions, eliminate unnecessary ones.") val compactLocals = Choice("compact-locals", "Eliminate empty slots in the sequence of local variables.") val copyPropagation = Choice("copy-propagation", "Eliminate redundant local variables and unused values (including closures). Enables unreachable-code.") + val redundantCasts = Choice("redundant-casts", "Eliminate redundant casts using a type propagation analysis.") val boxUnbox = Choice("box-unbox", "Eliminate box-unbox pairs within the same method (also tuples, xRefs, value class instances). Enables unreachable-code.") val nullnessTracking = Choice("nullness-tracking", "Track nullness / non-nullness of local variables and apply optimizations.") val closureInvocations = Choice("closure-invocations" , "Rewrite closure invocations to the implementation method.") @@ -240,7 +241,7 @@ trait ScalaSettings extends AbsScalaSettings private val defaultChoices = List(unreachableCode) val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices) - private val methodChoices = List(unreachableCode, simplifyJumps, compactLocals, copyPropagation, boxUnbox, nullnessTracking, closureInvocations) + private val methodChoices = List(unreachableCode, simplifyJumps, compactLocals, copyPropagation, redundantCasts, boxUnbox, nullnessTracking, closureInvocations) val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) private val projectChoices = List(lMethod, inlineProject) @@ -261,6 +262,7 @@ trait ScalaSettings extends AbsScalaSettings def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps) def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals) def YoptCopyPropagation = Yopt.contains(YoptChoices.copyPropagation) + def YoptRedundantCasts = Yopt.contains(YoptChoices.redundantCasts) def YoptBoxUnbox = Yopt.contains(YoptChoices.boxUnbox) def YoptNullnessTracking = Yopt.contains(YoptChoices.nullnessTracking) def YoptClosureInvocations = Yopt.contains(YoptChoices.closureInvocations) diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala index 99041b5497..0f87280000 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala @@ -85,6 +85,6 @@ class ClosureOptimizerTest extends ClearAfterClass { assertSameCode(getSingleMethod(c, "t").instructions.dropNonOp, List(VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/collection/immutable/List", "head", "()Ljava/lang/Object;", false), TypeOp(CHECKCAST, "java/lang/String"), Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(Ljava/lang/String;)Ljava/lang/String;", false), - TypeOp(CHECKCAST, "java/lang/String"), Op(ARETURN))) + Op(ARETURN))) } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 7b3936f867..4c6b3ed286 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -1326,9 +1326,9 @@ class InlinerTest extends ClearAfterClass { // value class instantiation-extraction should be optimized by boxing elim assertEquals(getSingleMethod(c, "t3").instructions.summary, List( NEW, DUP, ICONST_1, "", ASTORE, - NEW, DUP, ALOAD, CHECKCAST, "x", + NEW, DUP, ALOAD, "x", "C$$$anonfun$4", - "", CHECKCAST, + "", "x", IRETURN)) assertEquals(getSingleMethod(c, "t4").instructions.summary, List( @@ -1432,9 +1432,41 @@ class InlinerTest extends ClearAfterClass { val List(c) = compile(code) assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(Op(ICONST_3), Op(ICONST_4), Op(IADD), Op(IRETURN))) assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(Op(ICONST_1), Op(ICONST_2), Op(IADD), Op(IRETURN))) - // tuple not yet eliminated due to null checks, casts + // tuple not yet eliminated due to null checks assert(getSingleMethod(c, "t3").instructions.exists(_.opcode == IFNONNULL)) - assert(getSingleMethod(c, "t4").instructions.exists(_.opcode == CHECKCAST)) + assert(getSingleMethod(c, "t4").instructions.exists(_.opcode == IFNULL)) assert(getSingleMethod(c, "t5").instructions.exists(_.opcode == IFNULL)) } + + @Test + def redundantCasts(): Unit = { + + // we go through the hoop of inlining the casts because erasure eliminates `asInstanceOf` calls + // that are statically known to succeed. For example the following cast is removed by erasure: + // `(if (b) c else d).asInstanceOf[C]` + + val code = + """class C { + | @inline final def asO(a: Any) = a.asInstanceOf[Object] + | @inline final def asC(a: Any) = a.asInstanceOf[C] + | @inline final def asD(a: Any) = a.asInstanceOf[D] + | + | def t1(c: C) = asC(c) // eliminated + | def t2(c: C) = asO(c) // eliminated + | def t3(c: Object) = asC(c) // not elimianted + | def t4(c: C, d: D, b: Boolean) = asC(if (b) c else d) // not eliminated: lub of two non-equal reference types approximated with Object + | def t5(c: C, d: D, b: Boolean) = asO(if (b) c else d) + | def t6(c: C, cs: Array[C], b: Boolean) = asO(if (b) c else cs) + |} + |class D extends C + """.stripMargin + val List(c, _) = compile(code) + def casts(m: String) = getSingleMethod(c, m).instructions collect { case TypeOp(CHECKCAST, tp) => tp } + assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t3").instructions.dropNonOp, List(VarOp(ALOAD, 1), TypeOp(CHECKCAST, "C"), Op(ARETURN))) + assertEquals(casts("t4"), List("C")) + assertEquals(casts("t5"), Nil) + assertEquals(casts("t6"), Nil) + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala index 94c9f55728..fda38fa2e8 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -472,8 +472,7 @@ class MethodLevelOptsTest extends ClearAfterClass { val List(c) = compileClasses(methodOptCompiler)(code) assertNoInvoke(getSingleMethod(c, "t1")) assertEquals(getSingleMethod(c, "t2").instructions.summary, List(ICONST_1, ICONST_3, IADD, IRETURN)) - // cannot eliminate boxes because of casts - assertEquals(getSingleMethod(c, "t3").instructions collect { case TypeOp(CHECKCAST, tp) => tp }, List("java/lang/Integer", "java/lang/Integer")) + assertEquals(getSingleMethod(c, "t3").instructions.summary, List(ICONST_3, ICONST_4, IADD, IRETURN)) assertEquals(getSingleMethod(c, "t4").instructions.summary, List(ICONST_3, "boxToInteger", ARETURN)) assertEquals(getSingleMethod(c, "t5").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( ("scala/runtime/BoxesRunTime", "boxToInteger"), @@ -482,11 +481,11 @@ class MethodLevelOptsTest extends ClearAfterClass { ("scala/Tuple2", "_1$mcI$sp"))) // cannot eliminate boxes because of null checks assert(getSingleMethod(c, "t6").instructions.exists(_.opcode == IFNONNULL)) - // cannot eliminate boxed because of casts and null checks + // cannot eliminate boxed because of null checks def castsNullChecks(m: String) = getSingleMethod(c, m).instructions collect { case op if op.opcode == IFNULL || op.opcode == CHECKCAST => op } assertEquals(castsNullChecks("t7"), - List(Jump(IFNULL, Label(29)), TypeOp(CHECKCAST, "scala/Tuple2"), Jump(IFNULL, Label(29)))) - assertEquals(castsNullChecks("t8").size, 7) + List(Jump(IFNULL, Label(28)), Jump(IFNULL, Label(28)))) + assertEquals(castsNullChecks("t8").size, 3) assertEquals(castsNullChecks("t9").size, 2) } } -- cgit v1.2.3 From eacaee2a2a5dde6bb3f38cd414259d3517976cf6 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Mon, 14 Dec 2015 10:37:54 +0100 Subject: Ensure that `-Yopt:l:none` takes precedence, tidy optimizer settings --- .../scala/tools/nsc/settings/ScalaSettings.scala | 45 ++++++++++++++-------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index 87a9c262f4..f64fe437e4 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -236,39 +236,50 @@ trait ScalaSettings extends AbsScalaSettings val inlineProject = Choice("inline-project", "Inline only methods defined in the files being compiled. Enables unreachable-code.") val inlineGlobal = Choice("inline-global", "Inline methods from any source, including classfiles on the compile classpath. Enables unreachable-code.") - val lNone = Choice("l:none", "Don't enable any optimizations.") + // note: unlike the other optimizer levels, "l:none" appears up in the `Yopt.value` set because it's not an expanding option (expandsTo is empty) + val lNone = Choice("l:none", "Disable optimizations. Takes precedence: `-Yopt:l:none,+box-unbox` / `-Yopt:l:none -Yopt:box-unbox` don't enable box-unbox.") private val defaultChoices = List(unreachableCode) - val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices) + val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString("", ",", "."), expandsTo = defaultChoices) private val methodChoices = List(unreachableCode, simplifyJumps, compactLocals, copyPropagation, redundantCasts, boxUnbox, nullnessTracking, closureInvocations) - val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices) + val lMethod = Choice("l:method", "Enable intra-method optimizations: "+ methodChoices.mkString("", ",", "."), expandsTo = methodChoices) private val projectChoices = List(lMethod, inlineProject) - val lProject = Choice("l:project", "Enable cross-method optimizations within the current project: "+ projectChoices.mkString(","), expandsTo = projectChoices) + val lProject = Choice("l:project", "Enable cross-method optimizations within the current project: "+ projectChoices.mkString("", ",", "."), expandsTo = projectChoices) private val classpathChoices = List(lProject, inlineGlobal) - val lClasspath = Choice("l:classpath", "Enable cross-method optimizations across the entire classpath: "+ classpathChoices.mkString(","), expandsTo = classpathChoices) + val lClasspath = Choice("l:classpath", "Enable cross-method optimizations across the entire classpath: "+ classpathChoices.mkString("", ",", "."), expandsTo = classpathChoices) } + // We don't use the `default` parameter of `MultiChoiceSetting`: it specifies the default values + // when `-Yopt` is passed without explicit choices. When `-Yopt` is not explicitly specified, the + // set `Yopt.value` is empty. val Yopt = MultiChoiceSetting( name = "-Yopt", helpArg = "optimization", descr = "Enable optimizations", domain = YoptChoices) - def YoptNone = Yopt.isSetByUser && Yopt.value.isEmpty - def YoptUnreachableCode = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode) - def YoptSimplifyJumps = Yopt.contains(YoptChoices.simplifyJumps) - def YoptCompactLocals = Yopt.contains(YoptChoices.compactLocals) - def YoptCopyPropagation = Yopt.contains(YoptChoices.copyPropagation) - def YoptRedundantCasts = Yopt.contains(YoptChoices.redundantCasts) - def YoptBoxUnbox = Yopt.contains(YoptChoices.boxUnbox) - def YoptNullnessTracking = Yopt.contains(YoptChoices.nullnessTracking) - def YoptClosureInvocations = Yopt.contains(YoptChoices.closureInvocations) - - def YoptInlineProject = Yopt.contains(YoptChoices.inlineProject) - def YoptInlineGlobal = Yopt.contains(YoptChoices.inlineGlobal) + private def optEnabled(choice: YoptChoices.Choice) = { + !Yopt.contains(YoptChoices.lNone) && { + Yopt.contains(choice) || + !Yopt.isSetByUser && YoptChoices.lDefault.expandsTo.contains(choice) + } + } + + def YoptNone = Yopt.contains(YoptChoices.lNone) + def YoptUnreachableCode = optEnabled(YoptChoices.unreachableCode) + def YoptSimplifyJumps = optEnabled(YoptChoices.simplifyJumps) + def YoptCompactLocals = optEnabled(YoptChoices.compactLocals) + def YoptCopyPropagation = optEnabled(YoptChoices.copyPropagation) + def YoptRedundantCasts = optEnabled(YoptChoices.redundantCasts) + def YoptBoxUnbox = optEnabled(YoptChoices.boxUnbox) + def YoptNullnessTracking = optEnabled(YoptChoices.nullnessTracking) + def YoptClosureInvocations = optEnabled(YoptChoices.closureInvocations) + + def YoptInlineProject = optEnabled(YoptChoices.inlineProject) + def YoptInlineGlobal = optEnabled(YoptChoices.inlineGlobal) def YoptInlinerEnabled = YoptInlineProject || YoptInlineGlobal def YoptBuildCallGraph = YoptInlinerEnabled || YoptClosureInvocations -- cgit v1.2.3 From 80b0660efbc10325a657812720f99aff7410f0ce Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 11 Dec 2015 15:30:15 +0100 Subject: Apply local optimization based on nullness information Optimize IFNULL branches to GOTO when (non-)nullness of the tested value is known statically. This enables unreachable code to be removed, which in turn enables boxes to be eliminated. Changed a test flag from `-Ynooptimise` to `-Yopt:l:classpath` - I still have to do this systematically, this will follow later. --- .../backend/jvm/analysis/NullnessAnalyzer.scala | 8 +- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 4 + .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 128 +++++++++++++++++++-- test/files/run/t7852.flags | 2 +- .../tools/nsc/backend/jvm/opt/InlinerTest.scala | 7 +- .../nsc/backend/jvm/opt/MethodLevelOptsTest.scala | 73 ++++++++++-- 6 files changed, 196 insertions(+), 26 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala index f9ac12eb4d..1078d0085e 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala @@ -113,13 +113,13 @@ final class NullnessInterpreter extends Interpreter[NullnessValue](Opcodes.ASM5) def ternaryOperation(insn: AbstractInsnNode, value1: NullnessValue, value2: NullnessValue, value3: NullnessValue): NullnessValue = UnknownValue1 - def naryOperation(insn: AbstractInsnNode, values: util.List[_ <: NullnessValue]): NullnessValue = (insn.getOpcode: @switch) match { - case Opcodes.MULTIANEWARRAY => + def naryOperation(insn: AbstractInsnNode, values: util.List[_ <: NullnessValue]): NullnessValue = insn match { + case mi: MethodInsnNode if isNonNullMethodInvocation(mi) => NotNullValue case _ => - // TODO: use a list of methods that are known to return non-null values - NullnessValue.unknown(insn) + if (insn.getOpcode == Opcodes.MULTIANEWARRAY) NotNullValue + else NullnessValue.unknown(insn) } def returnOperation(insn: AbstractInsnNode, value: NullnessValue, expected: NullnessValue): Unit = () diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index ebc6fa1c76..f83167eabf 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -354,6 +354,10 @@ object BytecodeUtils { isSideEffectFreeConstructorCall(insn) } + def isNonNullMethodInvocation(mi: MethodInsnNode): Boolean = { + isJavaBox(mi) || isScalaBox(mi) || isPredefAutoBox(mi) || isRefCreate(mi) || isRefZero(mi) + } + private val srBoxesRunTimeName = "scala/runtime/BoxesRunTime" private val boxToMethods = Map( diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 3b03a12405..a29961cdd7 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -9,7 +9,7 @@ package opt import scala.annotation.{tailrec, switch} import scala.tools.asm.Type -import scala.tools.asm.tree.analysis.BasicInterpreter +import scala.tools.asm.tree.analysis.{BasicInterpreter, Frame} import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import scala.collection.{mutable, immutable} @@ -28,6 +28,13 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * The optimizations marked UPSTREAM enable optimizations that were already executed, so they cause * another iteration in the fixpoint loop. * + * nullness optimizations: rewrite null-checking branches to GOTO if nullness is known + * + enables downstream + * - unreachable code (null / non-null branch becomes unreachable) + * - box-unbox elimination (may render an escaping consumer of a box unreachable) + * - stale stores (aload x is replaced by aconst_null if it's known null) + * - simplify jumps (replaces conditional jumps by goto, so may enable goto chains) + * * unreachable code / DCE (removes instructions of basic blocks to which there is no branch) * + enables downstream: * - stale stores (loads may be eliminated, removing consumers of a store) @@ -41,6 +48,8 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * * box-unbox elimination (eliminates box-unbox pairs withing the same method) * + enables UPSTREAM: + * - nullness optimizations (a box extraction operation (unknown nullness) may be rewritten to + * a read of a non-null local. example in doc comment of box-unbox implementation) * - further box-unbox elimination (e.g. an Integer stored in a Tuple; eliminating the tuple may * enable eliminating the Integer) * + enables downstream: @@ -223,6 +232,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { * Returns a pair of booleans (codeChanged, requireEliminateUnusedLocals). */ def removalRound( + requestNullness: Boolean, requestDCE: Boolean, requestBoxUnbox: Boolean, requestStaleStores: Boolean, @@ -231,16 +241,20 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { maxRecursion: Int = 10): (Boolean, Boolean) = { if (maxRecursion == 0) return (false, false) + // NULLNESS OPTIMIZATIONS + val runNullness = compilerSettings.YoptNullnessTracking && requestNullness + val nullnessOptChanged = runNullness && nullnessOptimizations(method, ownerClassName) + // UNREACHABLE CODE // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores, // boxUnboxElimination) require not having unreachable instructions (null frames). - val runDCE = (compilerSettings.YoptUnreachableCode && requestDCE) || + val runDCE = (compilerSettings.YoptUnreachableCode && (requestDCE || nullnessOptChanged)) || compilerSettings.YoptBoxUnbox || compilerSettings.YoptCopyPropagation val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) // BOX-UNBOX - val runBoxUnbox = compilerSettings.YoptBoxUnbox && requestBoxUnbox + val runBoxUnbox = compilerSettings.YoptBoxUnbox && (requestBoxUnbox || nullnessOptChanged) val boxUnboxChanged = runBoxUnbox && boxUnboxElimination(method, ownerClassName) // COPY PROPAGATION @@ -248,7 +262,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val copyPropChanged = runCopyProp && copyProp(method, ownerClassName) // STALE STORES - val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || codeRemoved || boxUnboxChanged || copyPropChanged) + val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || nullnessOptChanged || codeRemoved || boxUnboxChanged || copyPropChanged) val storesRemoved = runStaleStores && eliminateStaleStores(method, ownerClassName) // REDUNDANT CASTS @@ -274,13 +288,15 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val jumpsChanged = runSimplifyJumps && simplifyJumps(method) // See doc comment in the beginning of this file (optimizations marked UPSTREAM) + val runNullnessAgain = boxUnboxChanged val runDCEAgain = liveHandlerRemoved || jumpsChanged val runBoxUnboxAgain = boxUnboxChanged || castRemoved || pushPopRemoved || liveHandlerRemoved val runStaleStoresAgain = pushPopRemoved val runStoreLoadAgain = jumpsChanged - val runAgain = runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runStoreLoadAgain + val runAgain = runNullnessAgain || runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runStoreLoadAgain val downstreamRequireEliminateUnusedLocals = runAgain && removalRound( + requestNullness = runNullnessAgain, requestDCE = runDCEAgain, requestBoxUnbox = runBoxUnboxAgain, requestStaleStores = runStaleStoresAgain, @@ -289,20 +305,22 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { maxRecursion = maxRecursion - 1)._2 val requireEliminateUnusedLocals = downstreamRequireEliminateUnusedLocals || + nullnessOptChanged || // nullness opt may eliminate stores / loads, rendering a local unused codeRemoved || // see comment in method `methodOptimizations` boxUnboxChanged || // box-unbox renders locals (holding boxes) unused storesRemoved || storeLoadRemoved || handlersRemoved - val codeChanged = codeRemoved || boxUnboxChanged || castRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged + val codeChanged = nullnessOptChanged || codeRemoved || boxUnboxChanged || castRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged (codeChanged, requireEliminateUnusedLocals) } - val (dceBoxesCopypropPushpopOrJumpsChanged, requireEliminateUnusedLocals) = if (AsmAnalyzer.sizeOKForBasicValue(method)) { + val (nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged, requireEliminateUnusedLocals) = if (AsmAnalyzer.sizeOKForBasicValue(method)) { // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE // here is more thorough than `minimalRemoveUnreachableCode` that run before inlining. val r = removalRound( + requestNullness = true, requestDCE = true, requestBoxUnbox = true, requestStaleStores = true, @@ -328,7 +346,101 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations) assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) - dceBoxesCopypropPushpopOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + } + + /** + * Apply various optimizations based on nullness analysis information. + * - IFNULL / IFNONNULL are rewritten to GOTO if nullness is known + * - IF_ACMPEQ / IF_ACMPNE are rewritten to GOTO if the both references are known null, or if + * one is known null and the other known not-null + * - ALOAD is replaced by ACONST_NULL if the local is known to hold null + * - ASTORE of null is removed if the local is known to hold null + * - INSTANCEOF of null is replaced by `ICONST_0` + * - scala.runtime.BoxesRunTime.unboxToX(null) is rewritten to a zero-value load + */ + def nullnessOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = { + AsmAnalyzer.sizeOKForNullness(method) && { + lazy val nullnessAnalyzer = new AsmAnalyzer(method, ownerClassName, new NullnessAnalyzer) + + // When running nullness optimizations the method may still have unreachable code. Analyzer + // frames of unreachable instructions are `null`. + def frameAt(insn: AbstractInsnNode): Option[Frame[NullnessValue]] = Option(nullnessAnalyzer.frameAt(insn)) + + def nullness(insn: AbstractInsnNode, slot: Int): Option[NullnessValue] = { + frameAt(insn).map(_.getValue(slot)) + } + + def isNull(insn: AbstractInsnNode, slot: Int) = nullness(insn, slot).contains(NullValue) + + // cannot change instructions while iterating, it gets the analysis out of synch (indexed by instructions) + val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + + val it = method.instructions.iterator() + while (it.hasNext) it.next() match { + case vi: VarInsnNode if isNull(vi, vi.`var`) => + if (vi.getOpcode == ALOAD) + toReplace(vi) = List(new InsnNode(ACONST_NULL)) + else if (vi.getOpcode == ASTORE) + for (frame <- frameAt(vi) if frame.peekStack(0) == NullValue) + toReplace(vi) = List(getPop(1)) + + case ji: JumpInsnNode => + val isIfNull = ji.getOpcode == IFNULL + val isIfNonNull = ji.getOpcode == IFNONNULL + if (isIfNull || isIfNonNull) for (frame <- frameAt(ji)) { + val nullness = frame.peekStack(0) + val taken = nullness == NullValue && isIfNull || nullness == NotNullValue && isIfNonNull + val avoided = nullness == NotNullValue && isIfNull || nullness == NullValue && isIfNonNull + if (taken || avoided) { + val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil + toReplace(ji) = getPop(1) :: jump + } + } else { + val isIfEq = ji.getOpcode == IF_ACMPEQ + val isIfNe = ji.getOpcode == IF_ACMPNE + if (isIfEq || isIfNe) for (frame <- frameAt(ji)) { + val aNullness = frame.peekStack(1) + val bNullness = frame.peekStack(0) + val eq = aNullness == NullValue && bNullness == NullValue + val ne = aNullness == NullValue && bNullness == NotNullValue || aNullness == NotNullValue && bNullness == NullValue + val taken = isIfEq && eq || isIfNe && ne + val avoided = isIfEq && ne || isIfNe && eq + if (taken || avoided) { + val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil + toReplace(ji) = getPop(1) :: getPop(1) :: jump + } + } + } + + case ti: TypeInsnNode => + if (ti.getOpcode == INSTANCEOF) for (frame <- frameAt(ti) if frame.peekStack(0) == NullValue) { + toReplace(ti) = List(getPop(1), new InsnNode(ICONST_0)) + } + + case mi: MethodInsnNode => + if (isScalaUnbox(mi)) for (frame <- frameAt(mi) if frame.peekStack(0) == NullValue) { + toReplace(mi) = List( + getPop(1), + loadZeroForTypeSort(Type.getReturnType(mi.desc).getSort)) + } + + case _ => + } + + def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match { + case mi: MethodInsnNode => callGraph.removeCallsite(mi, method) + case _ => + } + + for ((oldOp, newOps) <- toReplace) { + for (newOp <- newOps) method.instructions.insertBefore(oldOp, newOp) + method.instructions.remove(oldOp) + removeFromCallGraph(oldOp) + } + + toReplace.nonEmpty + } } /** diff --git a/test/files/run/t7852.flags b/test/files/run/t7852.flags index f6262fd3e0..bc22511cff 100644 --- a/test/files/run/t7852.flags +++ b/test/files/run/t7852.flags @@ -1 +1 @@ --Ynooptimise +-Yopt:l:none diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 4c6b3ed286..192a036d5b 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -1432,10 +1432,9 @@ class InlinerTest extends ClearAfterClass { val List(c) = compile(code) assertSameCode(getSingleMethod(c, "t1").instructions.dropNonOp, List(Op(ICONST_3), Op(ICONST_4), Op(IADD), Op(IRETURN))) assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(Op(ICONST_1), Op(ICONST_2), Op(IADD), Op(IRETURN))) - // tuple not yet eliminated due to null checks - assert(getSingleMethod(c, "t3").instructions.exists(_.opcode == IFNONNULL)) - assert(getSingleMethod(c, "t4").instructions.exists(_.opcode == IFNULL)) - assert(getSingleMethod(c, "t5").instructions.exists(_.opcode == IFNULL)) + assertSameCode(getSingleMethod(c, "t3").instructions.dropNonOp, List(Op(ICONST_1), Op(ICONST_3), Op(ISUB), Op(IRETURN))) + assertNoInvoke(getSingleMethod(c, "t4")) + assertNoInvoke(getSingleMethod(c, "t5")) } @Test diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala index fda38fa2e8..5ee52ff78f 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -338,7 +338,7 @@ class MethodLevelOptsTest extends ClearAfterClass { assertNoInvoke(getSingleMethod(c, "t5")) assertNoInvoke(getSingleMethod(c, "t6")) assertNoInvoke(getSingleMethod(c, "t7")) - assertEquals(getSingleMethod(c, "t8").instructions.summary, List(ACONST_NULL, "unboxToInt", IRETURN)) + assertEquals(getSingleMethod(c, "t8").instructions.summary, List(ICONST_0, IRETURN)) assertNoInvoke(getSingleMethod(c, "t9")) // t10: no invocation of unbox assertEquals(getSingleMethod(c, "t10").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( @@ -479,13 +479,68 @@ class MethodLevelOptsTest extends ClearAfterClass { ("scala/runtime/BoxesRunTime", "boxToInteger"), ("C", "tpl"), ("scala/Tuple2", "_1$mcI$sp"))) - // cannot eliminate boxes because of null checks - assert(getSingleMethod(c, "t6").instructions.exists(_.opcode == IFNONNULL)) - // cannot eliminate boxed because of null checks - def castsNullChecks(m: String) = getSingleMethod(c, m).instructions collect { case op if op.opcode == IFNULL || op.opcode == CHECKCAST => op } - assertEquals(castsNullChecks("t7"), - List(Jump(IFNULL, Label(28)), Jump(IFNULL, Label(28)))) - assertEquals(castsNullChecks("t8").size, 3) - assertEquals(castsNullChecks("t9").size, 2) + assertEquals(getSingleMethod(c, "t6").instructions.summary, List(ICONST_1, ICONST_2, ISUB, IRETURN)) + assertEquals(getSingleMethod(c, "t7").instructions.summary, List( + ICONST_1, ICONST_2, ISTORE, ISTORE, + ICONST_3, ISTORE, + ILOAD, ILOAD, IADD, ILOAD, IADD, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t8")) + assertNoInvoke(getSingleMethod(c, "t9")) + } + + @Test + def nullnessOpts(): Unit = { + val code = + """class C { + | def t1 = { + | val a = new C + | if (a == null) + | println() // eliminated + | a + | } + | + | def t2 = null.asInstanceOf[Long] // replaced by zero value + | + | def t3 = { + | val t = (1, 3) + | val a = null + | if (t ne a) t._1 + | else throw new Error() + | } + | + | def t4 = { + | val i = Integer.valueOf(1) + | val a = null + | if (i eq a) throw new Error() + | else i.toInt + | } + | + | def t5 = { + | val i = runtime.DoubleRef.zero() + | if (i == null) throw new Error() + | else i.elem + | } + | + | def t6 = { + | var a = null + | var i = null + | a = i // eliminated (store of null to variable that is already null) + | a // replaced by ACONST_NULL (load of variable that is known null) + | } + | + | def t7 = { + | val a = null + | a.isInstanceOf[String] // eliminated, replaced by 0 (null.isInstanceOf is always false) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(getSingleMethod(c, "t1").instructions.summary, List(NEW, DUP, "", ARETURN)) + assertSameCode(getSingleMethod(c, "t2").instructions.dropNonOp, List(Op(LCONST_0), Op(LRETURN))) + assertSameCode(getSingleMethod(c, "t3").instructions.dropNonOp, List(Op(ICONST_1), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t4").instructions.dropNonOp, List(Op(ICONST_1), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t5").instructions.dropNonOp, List(Op(DCONST_0), Op(DRETURN))) + assertSameCode(getSingleMethod(c, "t6").instructions.dropNonOp, List(Op(ACONST_NULL), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t7").instructions.dropNonOp, List(Op(ICONST_0), Op(IRETURN))) } } -- cgit v1.2.3 From 55faa0d06fb4c2f5001626b10d0657cc337b5c37 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Tue, 15 Dec 2015 15:55:11 +0100 Subject: Harden push-pop elimination when eliminating constants Ensure that LDC instructions are only eliminated when the loaded value is a numeric or string constant. Removing other literals may remove a potential failure. [1] lists the kind of values that can be loaded by a LDC [1] https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.ldc --- src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index a29961cdd7..1668d10da9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -845,10 +845,14 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { if (isNewForSideEffectFreeConstructor(prod)) toRemove += prod else popAfterProd() - case LDC => - // don't remove class literals: keep the potential NoClassDefFoundError - if (prod.asInstanceOf[LdcInsnNode].cst.isInstanceOf[Type]) popAfterProd() - else toRemove += prod + case LDC => prod.asInstanceOf[LdcInsnNode].cst match { + case _: java.lang.Integer | _: java.lang.Float | _: java.lang.Long | _: java.lang.Double | _: String => + toRemove += prod + + case _ => + // don't remove class literals, method types, method handles: keep a potential NoClassDefFoundError + popAfterProd() + } case NEWARRAY | ANEWARRAY => toRemove += prod -- cgit v1.2.3 From 118aa366782f9f27b13710122686c609c6731dfd Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Wed, 16 Dec 2015 15:58:30 +0100 Subject: Move copy propagation implementation to a separate file --- .../scala/tools/nsc/backend/jvm/opt/CopyProp.scala | 645 +++++++++++++++++++++ .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 627 +------------------- 2 files changed, 650 insertions(+), 622 deletions(-) create mode 100644 src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala new file mode 100644 index 0000000000..149742888a --- /dev/null +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala @@ -0,0 +1,645 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2014 LAMP/EPFL + * @author Martin Odersky + */ + +package scala.tools.nsc +package backend.jvm +package opt + +import scala.annotation.{switch, tailrec} +import scala.tools.asm.tree.analysis.BasicInterpreter +import scala.tools.asm.Type +import scala.tools.asm.Opcodes._ +import scala.tools.asm.tree._ +import scala.collection.mutable +import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.analysis._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ + +class CopyProp[BT <: BTypes](val btypes: BT) { + import btypes._ + import backendUtils._ + + + /** + * For every `xLOAD n`, find all local variable slots that are aliases of `n` using an + * AliasingAnalyzer and change the instruction to `xLOAD m` where `m` is the smallest alias. + * This leaves behind potentially stale `xSTORE n` instructions, which are then eliminated + * by [[eliminateStaleStores]]. + */ + def copyPropagation(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForAliasing(method) && { + var changed = false + val numParams = parametersSize(method) + lazy val aliasAnalysis = new AsmAnalyzer(method, owner, new AliasingAnalyzer(new BasicInterpreter)) + + // Remember locals that are used in a `LOAD` instruction. Assume a program has two LOADs: + // + // ... + // LOAD 3 // aliases of 3 here: <3> + // ... + // LOAD 1 // aliases of 1 here: <1, 3> + // + // In this example, we should change the second load from 1 to 3, which might render the + // local variable 1 unused. + val knownUsed = new Array[Boolean](method.maxLocals) + + def usedOrMinAlias(it: IntIterator, init: Int): Int = { + if (knownUsed(init)) init + else { + var r = init + while (it.hasNext) { + val n = it.next() + // knownUsed.lenght is the number of locals, `n` may be a stack slot + if (n < knownUsed.length && knownUsed(n)) return n + if (n < r) r = n + } + r + } + } + + val it = method.instructions.iterator + while (it.hasNext) it.next() match { + case vi: VarInsnNode if vi.`var` >= numParams && isLoad(vi) => + val aliases = aliasAnalysis.frameAt(vi).asInstanceOf[AliasingFrame[_]].aliasesOf(vi.`var`) + if (aliases.size > 1) { + val alias = usedOrMinAlias(aliases.iterator, vi.`var`) + if (alias != -1) { + changed = true + vi.`var` = alias + } + } + knownUsed(vi.`var`) = true + + case _ => + } + + changed + } + } + + /** + * Eliminate `xSTORE` instructions that have no consumer. If the instruction can be completely + * eliminated, it is replaced by a POP. The [[eliminatePushPop]] cleans up unnecessary POPs. + * + * Note that an `ASOTRE` can not always be eliminated: it removes a reference to the object that + * is currently stored in that local, which potentially frees it for GC (SI-5313). Therefore + * we replace such stores by `POP; ACONST_NULL; ASTORE x`. + */ + def eliminateStaleStores(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + lazy val prodCons = new ProdConsAnalyzer(method, owner) + def hasNoCons(varIns: AbstractInsnNode, slot: Int) = prodCons.consumersOfValueAt(varIns.getNext, slot).isEmpty + + // insns to delete: IINC that have no consumer + val toDelete = mutable.ArrayBuffer.empty[IincInsnNode] + + // xSTORE insns to be replaced by POP or POP2 + val storesToDrop = mutable.ArrayBuffer.empty[VarInsnNode] + + // ASTORE insn that have no consumer. + // - if the local is not live, the store is replaced by POP + // - otherwise, pop the argument value and store NULL instead. Unless the boolean field is + // `true`: then the store argument is already known to be ACONST_NULL. + val toNullOut = mutable.ArrayBuffer.empty[(VarInsnNode, Boolean)] + + // `true` for variables that are known to be live + val liveVars = new Array[Boolean](method.maxLocals) + + val it = method.instructions.iterator + while (it.hasNext) it.next() match { + case vi: VarInsnNode if isStore(vi) && hasNoCons(vi, vi.`var`) => + val canElim = vi.getOpcode != ASTORE || { + val currentFieldValueProds = prodCons.initialProducersForValueAt(vi, vi.`var`) + currentFieldValueProds.size == 1 && (currentFieldValueProds.head match { + case ParameterProducer(0) => !isStaticMethod(method) // current field value is `this`, which won't be gc'd anyway + case _: UninitializedLocalProducer => true // field is not yet initialized, so current value cannot leak + case _ => false + }) + } + if (canElim) storesToDrop += vi + else { + val prods = prodCons.producersForValueAt(vi, prodCons.frameAt(vi).stackTop) + val isStoreNull = prods.size == 1 && prods.head.getOpcode == ACONST_NULL + toNullOut += ((vi, isStoreNull)) + } + + case ii: IincInsnNode if hasNoCons(ii, ii.`var`) => + toDelete += ii + + case vi: VarInsnNode => + liveVars(vi.`var`) = true + + case ii: IincInsnNode => + liveVars(ii.`var`) = true + + case _ => + } + + def replaceByPop(vi: VarInsnNode): Unit = { + val size = if (isSize2LoadOrStore(vi.getOpcode)) 2 else 1 + method.instructions.set(vi, getPop(size)) + } + + toDelete foreach method.instructions.remove + + storesToDrop foreach replaceByPop + + for ((vi, isStoreNull) <- toNullOut) { + if (!liveVars(vi.`var`)) replaceByPop(vi) // can drop `ASTORE x` where x has only dead stores + else { + if (!isStoreNull) { + val prev = vi.getPrevious + method.instructions.insert(prev, new InsnNode(ACONST_NULL)) + method.instructions.insert(prev, getPop(1)) + } + } + } + + toDelete.nonEmpty || storesToDrop.nonEmpty || toNullOut.nonEmpty + } + } + + /** + * When a POP instruction has a single producer, remove the POP and eliminate the producer by + * bubbling up the POPs. For example, given + * ILOAD 1; ILOAD 2; IADD; POP + * we first eliminate the POP, then the IADD, then its inputs, so the entire sequence goes away. + * If a producer cannot be eliminated (need to keep side-effects), a POP is inserted. + * + * A special case eliminates the creation of unused objects with side-effect-free constructors: + * NEW scala/Tuple1; DUP; ALOAD 0; INVOKESPECIAL scala/Tuple1.; POP + * The POP has a signle producer (the DUP), it's easy to eliminate these two. A special case + * is needed to eliminate the INVOKESPECIAL and NEW. + */ + def eliminatePushPop(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForSourceValue(method) && { + // A queue of instructions producing a value that has to be eliminated. If possible, the + // instruction (and its inputs) will be removed, otherwise a POP is inserted after + val queue = mutable.Queue.empty[ProducedValue] + // Contains constructor invocations for values that can be eliminated if unused. + val sideEffectFreeConstructorCalls = mutable.ArrayBuffer.empty[MethodInsnNode] + + // instructions to remove (we don't change the bytecode while analyzing it. this allows + // running the ProdConsAnalyzer only once.) + val toRemove = mutable.Set.empty[AbstractInsnNode] + // instructions to insert before some instruction + val toInsertBefore = mutable.Map.empty[AbstractInsnNode, List[InsnNode]] + // an instruction to insert after some instruction + val toInsertAfter = mutable.Map.empty[AbstractInsnNode, AbstractInsnNode] + + lazy val prodCons = new ProdConsAnalyzer(method, owner) + + /** + * Returns the producers for the stack value `inputSlot` consumed by `cons`, if the consumer + * instruction is the only consumer for all of these producers. + * + * If a producer has multiple consumers, or the value is the caught exception in a catch + * block, this method returns Set.empty. + */ + def producersIfSingleConsumer(cons: AbstractInsnNode, inputSlot: Int): Set[AbstractInsnNode] = { + /** + * True if the values produced by `prod` are all the same. Most instructions produce a single + * value. DUP and DUP2 (with a size-2 input) produce two equivalent values. However, there + * are some exotic instructions that produce multiple non-equal values (DUP_X1, SWAP, ...). + * + * Assume we have `DUP_X2; POP`. In order to remove the `POP` we need to change the DUP_X2 + * into something else, which is not straightforward. + * + * Since scalac never emits any of those exotic bytecodes, we don't optimize them. + */ + def producerHasSingleOutput(prod: AbstractInsnNode): Boolean = prod match { + case _: ExceptionProducer[_] | _: UninitializedLocalProducer => + // POP of an exception in a catch block cannot be removed. For an uninitialized local, + // there should not be a consumer. We are conservative and include it here, so the + // producer would not be removed. + false + + case _: ParameterProducer => + true + + case _ => (prod.getOpcode: @switch) match { + case DUP => true + case DUP2 => prodCons.frameAt(prod).peekStack(0).getSize == 2 + case _ => InstructionStackEffect.prod(InstructionStackEffect.forAsmAnalysis(prod, prodCons.frameAt(prod))) == 1 + } + } + + val prods = prodCons.producersForValueAt(cons, inputSlot) + val singleConsumer = prods forall { prod => + producerHasSingleOutput(prod) && { + // for DUP / DUP2, we only consider the value that is actually consumed by cons + val conss = prodCons.consumersOfValueAt(prod.getNext, inputSlot) + conss.size == 1 && conss.head == cons + } + } + if (singleConsumer) prods else Set.empty + } + + /** + * For a POP instruction that is the single consumer of its producers, remove the POP and + * enqueue the producers. + */ + def handleInitialPop(pop: AbstractInsnNode): Unit = { + val prods = producersIfSingleConsumer(pop, prodCons.frameAt(pop).stackTop) + if (prods.nonEmpty) { + toRemove += pop + val size = if (pop.getOpcode == POP2) 2 else 1 + queue ++= prods.map(ProducedValue(_, size)) + } + } + + /** + * Traverse the method in its initial state and collect all POP instructions and side-effect + * free constructor invocations that can be eliminated. + */ + def collectInitialPopsAndPureConstrs(): Unit = { + val it = method.instructions.iterator + while (it.hasNext) { + val insn = it.next() + (insn.getOpcode: @switch) match { + case POP | POP2 => + handleInitialPop(insn) + + case INVOKESPECIAL => + val mi = insn.asInstanceOf[MethodInsnNode] + if (isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi + + case _ => + } + } + } + + /** + * Eliminate the `numArgs` inputs of the instruction `prod` (which was eliminated). Fo + * each input value + * - if the `prod` instruction is the single consumer, enqueue the producers of the input + * - otherwise, insert a POP instruction to POP the input value + */ + def handleInputs(prod: AbstractInsnNode, numArgs: Int): Unit = { + val frame = prodCons.frameAt(prod) + val pops = mutable.ListBuffer.empty[InsnNode] + @tailrec def handle(stackOffset: Int): Unit = { + if (stackOffset >= 0) { + val prods = producersIfSingleConsumer(prod, frame.stackTop - stackOffset) + val nSize = frame.peekStack(stackOffset).getSize + if (prods.isEmpty) pops append getPop(nSize) + else queue ++= prods.map(ProducedValue(_, nSize)) + handle(stackOffset - 1) + } + } + handle(numArgs - 1) // handle stack offsets (numArgs - 1) to 0 + if (pops.nonEmpty) toInsertBefore(prod) = pops.toList + } + + /** + * Eliminate the closure value produced by `indy`. If the SAM type is known to construct + * without side-effects (e.g. scala/runtime/java8/JFunctionN), the `indy` and its inputs + * are eliminated, otherwise a POP is inserted. + */ + def handleClosureInst(indy: InvokeDynamicInsnNode): Unit = { + if (isrJFunctionType(Type.getReturnType(indy.desc).getInternalName)) { + toRemove += indy + callGraph.removeClosureInstantiation(indy, method) + handleInputs(indy, Type.getArgumentTypes(indy.desc).length) + } else { + toInsertAfter(indy) = getPop(1) + } + } + + def runQueue(): Unit = while (queue.nonEmpty) { + val ProducedValue(prod, size) = queue.dequeue() + + def prodString = s"Producer ${AsmUtils textify prod}@${method.instructions.indexOf(prod)}\n${AsmUtils textify method}" + def popAfterProd(): Unit = toInsertAfter(prod) = getPop(size) + + (prod.getOpcode: @switch) match { + case ACONST_NULL | ICONST_M1 | ICONST_0 | ICONST_1 | ICONST_2 | ICONST_3 | ICONST_4 | ICONST_5 | LCONST_0 | LCONST_1 | FCONST_0 | FCONST_1 | FCONST_2 | DCONST_0 | DCONST_1 | + BIPUSH | SIPUSH | ILOAD | LLOAD | FLOAD | DLOAD | ALOAD=> + toRemove += prod + + case opc @ (DUP | DUP2) => + assert(opc != 2 || size == 2, s"DUP2 for two size-1 values; $prodString") // ensured in method `producerHasSingleOutput` + if (toRemove(prod)) + // the DUP is already scheduled for removal because one of its consumers is a POP. + // now the second consumer is also a POP, so we need to eliminate the DUP's input. + handleInputs(prod, 1) + else + toRemove += prod + + case DUP_X1 | DUP_X2 | DUP2_X1 | DUP2_X2 | SWAP => + // these are excluded in method `producerHasSingleOutput` + assert(false, s"Cannot eliminate value pushed by an instruction with multiple output values; $prodString") + + case IDIV | LDIV | IREM | LREM => + popAfterProd() // keep potential division by zero + + case IADD | LADD | FADD | DADD | ISUB | LSUB | FSUB | DSUB | IMUL | LMUL | FMUL | DMUL | FDIV | DDIV | FREM | DREM | + LSHL | LSHR | LUSHR | + IAND | IOR | IXOR | LAND | LOR | LXOR | + LCMP | FCMPL | FCMPG | DCMPL | DCMPG => + toRemove += prod + handleInputs(prod, 2) + + case INEG | LNEG | FNEG | DNEG | + I2L | I2F | I2D | L2I | L2F | L2D | F2I | F2L | F2D | D2I | D2L | D2F | I2B | I2C | I2S => + toRemove += prod + handleInputs(prod, 1) + + case GETFIELD | GETSTATIC => + // TODO eliminate side-effect free module loads (https://github.com/scala/scala-dev/issues/16) + if (isBoxedUnit(prod)) toRemove += prod + else popAfterProd() // keep potential class initialization (static field) or NPE (instance field) + + case INVOKEVIRTUAL | INVOKESPECIAL | INVOKESTATIC | INVOKEINTERFACE => + val methodInsn = prod.asInstanceOf[MethodInsnNode] + if (isSideEffectFreeCall(methodInsn)) { + toRemove += prod + callGraph.removeCallsite(methodInsn, method) + val receiver = if (methodInsn.getOpcode == INVOKESTATIC) 0 else 1 + handleInputs(prod, Type.getArgumentTypes(methodInsn.desc).length + receiver) + } else + popAfterProd() + + case INVOKEDYNAMIC => + prod match { + case callGraph.LambdaMetaFactoryCall(indy, _, _, _) => handleClosureInst(indy) + case _ => popAfterProd() + } + + case NEW => + if (isNewForSideEffectFreeConstructor(prod)) toRemove += prod + else popAfterProd() + + case LDC => prod.asInstanceOf[LdcInsnNode].cst match { + case _: java.lang.Integer | _: java.lang.Float | _: java.lang.Long | _: java.lang.Double | _: String => + toRemove += prod + + case _ => + // don't remove class literals, method types, method handles: keep a potential NoClassDefFoundError + popAfterProd() + } + + case NEWARRAY | ANEWARRAY => + toRemove += prod + handleInputs(prod, 1) + + case MULTIANEWARRAY => + toRemove += prod + handleInputs(prod, prod.asInstanceOf[MultiANewArrayInsnNode].dims) + + case _ => + popAfterProd() + } + } + + // there are two cases when we can eliminate a constructor call: + // - NEW T; INVOKESPECIAL T. -- there's no DUP, the new object is consumed only by the constructor) + // - NEW T; DUP; INVOKESPECIAL T., where the DUP will be removed + def eliminateUnusedPureConstructorCalls(): Boolean = { + var changed = false + + def removeConstructorCall(mi: MethodInsnNode): Unit = { + toRemove += mi + callGraph.removeCallsite(mi, method) + sideEffectFreeConstructorCalls -= mi + changed = true + } + + for (mi <- sideEffectFreeConstructorCalls.toList) { // toList to allow removing elements while traversing + val frame = prodCons.frameAt(mi) + val stackTop = frame.stackTop + val numArgs = Type.getArgumentTypes(mi.desc).length + val receiverProds = producersIfSingleConsumer(mi, stackTop - numArgs) + if (receiverProds.size == 1) { + val receiverProd = receiverProds.head + if (receiverProd.getOpcode == NEW) { + removeConstructorCall(mi) + handleInputs(mi, numArgs + 1) // removes the producers of args and receiver + } else if (receiverProd.getOpcode == DUP && toRemove.contains(receiverProd)) { + val dupProds = producersIfSingleConsumer(receiverProd, prodCons.frameAt(receiverProd).stackTop) + if (dupProds.size == 1 && dupProds.head.getOpcode == NEW) { + removeConstructorCall(mi) + handleInputs(mi, numArgs) // removes the producers of args. the producer of the receiver is DUP and already in toRemove. + queue += ProducedValue(dupProds.head, 1) // removes the NEW (which is NOT the producer of the receiver!) + } + } + } + } + changed + } + + collectInitialPopsAndPureConstrs() + + // eliminating producers enables eliminating unused constructor calls (when a DUP gets removed). + // vice-versa, eliminating a constructor call adds producers of constructor parameters to the queue. + // so the two run in a loop. + runQueue() + while (eliminateUnusedPureConstructorCalls()) + runQueue() + + var changed = false + toInsertAfter foreach { + case (target, insn) => + nextExecutableInstructionOrLabel(target) match { + // `insn` is of type `InsnNode`, so we only need to check the Opcode when comparing to another instruction + case Some(next) if next.getOpcode == insn.getOpcode && toRemove(next) => + // Inserting and removing a POP at the same place should not enable `changed`. This happens + // when a POP directly follows a producer that cannot be eliminated, e.g. INVOKESTATIC A.m ()I; POP + // The POP is initially added to `toRemove`, and the `INVOKESTATIC` producer is added to the queue. + // Because the producer cannot be elided, a POP is added to `toInsertAfter`. + toRemove -= next + + case _ => + changed = true + method.instructions.insert(target, insn) + } + } + toInsertBefore foreach { + case (target, insns) => + changed = true + insns.foreach(method.instructions.insertBefore(target, _)) + } + toRemove foreach { insn => + changed = true + method.instructions.remove(insn) + } + changed + } + } + + case class ProducedValue(producer: AbstractInsnNode, size: Int) { + override def toString = s"<${AsmUtils textify producer}>" + } + + /** + * Remove `xSTORE n; xLOAD n` paris if + * - the local variable n is not used anywhere else in the method (1), and + * - there are no executable instructions and no live labels (jump targets) between the two (2) + * + * Note: store-load pairs that cannot be eliminated could be replaced by `DUP; xSTORE n`, but + * that's just cosmetic and doesn't help for anything. + * + * (1) This could be made more precise by running a prodCons analysis and checking that the load + * is the only user of the store. Then we could eliminate the pair even if the variable is live + * (except for ASTORE, SI-5313). Not needing an analyzer is more efficient, and catches most + * cases. + * + * (2) The implementation uses a conservative estimation for liveness (if some instruction uses + * local n, then n is considered live in the entire method). In return, it doesn't need to run an + * Analyzer on the method, making it more efficient. + * + * This method also removes `ACONST_NULL; ASTORE n` if the local n is not live. This pattern is + * introduced by [[eliminateStaleStores]]. + * + * The implementation is a little tricky to support the following case: + * ISTORE 1; ISTORE 2; ILOAD 2; ACONST_NULL; ASTORE 3; ILOAD 1 + * The outer store-load pair can be removed if two the inner pairs can be. + */ + def eliminateStoreLoad(method: MethodNode): Boolean = { + val removePairs = mutable.Set.empty[RemovePair] + val liveVars = new Array[Boolean](method.maxLocals) + val liveLabels = mutable.Set.empty[LabelNode] + + def mkRemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]): RemovePair = { + val r = RemovePair(store, other, depends) + removePairs += r + r + } + + def registerLiveVarsLabels(insn: AbstractInsnNode): Unit = insn match { + case vi: VarInsnNode => liveVars(vi.`var`) = true + case ii: IincInsnNode => liveVars(ii.`var`) = true + case j: JumpInsnNode => liveLabels += j.label + case s: TableSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala + case s: LookupSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala + case _ => + } + + val pairStartStack = new mutable.Stack[(AbstractInsnNode, mutable.ListBuffer[RemovePairDependency])] + + def push(insn: AbstractInsnNode) = { + pairStartStack push ((insn, mutable.ListBuffer.empty)) + } + + def addDepends(dependency: RemovePairDependency) = if (pairStartStack.nonEmpty) { + val (_, depends) = pairStartStack.top + depends += dependency + } + + def completesStackTop(load: AbstractInsnNode) = isLoad(load) && pairStartStack.nonEmpty && { + pairStartStack.top match { + case (store: VarInsnNode, _) => store.`var` == load.asInstanceOf[VarInsnNode].`var` + case _ => false + } + } + + /** + * Try to pair `insn` with its correspondant on the stack + * - if the stack top is a store and `insn` is a corresponding load, create a pair + * - otherwise, check the two top stack values for `null; store`. if it matches, create + * a pair and continue pairing `insn` on the remaining stack + * - otherwise, empty the stack and mark the local variables in it live + */ + def tryToPairInstruction(insn: AbstractInsnNode): Unit = { + @tailrec def emptyStack(): Unit = if (pairStartStack.nonEmpty) { + registerLiveVarsLabels(pairStartStack.pop()._1) + emptyStack() + } + + @tailrec def tryPairing(): Unit = { + if (completesStackTop(insn)) { + val (store: VarInsnNode, depends) = pairStartStack.pop() + addDepends(mkRemovePair(store, insn, depends.toList)) + } else if (pairStartStack.nonEmpty) { + val (top, topDepends) = pairStartStack.pop() + if (pairStartStack.nonEmpty) { + (pairStartStack.top, top) match { + case ((ldNull: InsnNode, depends), store: VarInsnNode) if ldNull.getOpcode == ACONST_NULL && store.getOpcode == ASTORE => + pairStartStack.pop() + addDepends(mkRemovePair(store, ldNull, depends.toList)) + // example: store; (null; store;) (store; load;) load + // s1^ ^^^^^p1^^^^^ // p1 is added to s1's depends + // then: store; (null; store;) load + // s2^ ^^^^p2^^^^^ // p1 and p2 are added to s2's depends + topDepends foreach addDepends + tryPairing() + + case _ => + // empty the stack - a non-matching insn was found, cannot create any pairs to remove + registerLiveVarsLabels(insn) + registerLiveVarsLabels(top) + emptyStack() + } + } else { + // stack only has one element + registerLiveVarsLabels(insn) + registerLiveVarsLabels(top) + } + } else { + // stack is empty already + registerLiveVarsLabels(insn) + } + } + + tryPairing() + } + + + var insn = method.instructions.getFirst + + @tailrec def advanceToNextExecutableOrLabel(): Unit = { + insn = insn.getNext + if (insn != null && !isExecutable(insn) && !insn.isInstanceOf[LabelNode]) advanceToNextExecutableOrLabel() + } + + while (insn != null) { + insn match { + case _ if insn.getOpcode == ACONST_NULL => push(insn) + case vi: VarInsnNode if isStore(vi) => push(insn) + case label: LabelNode if pairStartStack.nonEmpty => addDepends(LabelNotLive(label)) + case _ => tryToPairInstruction(insn) + } + advanceToNextExecutableOrLabel() + } + + // elide RemovePairs that depend on live labels or other RemovePair that have to be elided. + // example: store 1; store 2; label x; load 2; load 1 + // if x is live, the inner pair has to be elided, causing the outer pair to be elided too. + + var doneEliding = false + + def elide(removePair: RemovePair) = { + doneEliding = false + liveVars(removePair.store.`var`) = true + removePairs -= removePair + } + + while (!doneEliding) { + doneEliding = true + for (removePair <- removePairs.toList) { + val slot = removePair.store.`var` + if (liveVars(slot)) elide(removePair) + else removePair.depends foreach { + case LabelNotLive(label) => if (liveLabels(label)) elide(removePair) + case other: RemovePair => if (!removePairs(other)) elide(removePair) + } + } + } + + for (removePair <- removePairs) { + method.instructions.remove(removePair.store) + method.instructions.remove(removePair.other) + } + + removePairs.nonEmpty + } +} + +trait RemovePairDependency +case class RemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]) extends RemovePairDependency { + override def toString = s"<${AsmUtils textify store},${AsmUtils textify other}> [$depends]" +} +case class LabelNotLive(label: LabelNode) extends RemovePairDependency diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 1668d10da9..8db922a635 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -9,7 +9,7 @@ package opt import scala.annotation.{tailrec, switch} import scala.tools.asm.Type -import scala.tools.asm.tree.analysis.{BasicInterpreter, Frame} +import scala.tools.asm.tree.analysis.Frame import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import scala.collection.{mutable, immutable} @@ -143,6 +143,9 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { val boxUnbox = new BoxUnbox(btypes) import boxUnbox._ + val copyProp = new CopyProp(btypes) + import copyProp._ + /** * Remove unreachable code from a method. * @@ -259,7 +262,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // COPY PROPAGATION val runCopyProp = compilerSettings.YoptCopyPropagation && (firstIteration || boxUnboxChanged) - val copyPropChanged = runCopyProp && copyProp(method, ownerClassName) + val copyPropChanged = runCopyProp && copyPropagation(method, ownerClassName) // STALE STORES val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || nullnessOptChanged || codeRemoved || boxUnboxChanged || copyPropChanged) @@ -495,457 +498,6 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { (changed, liveLabels) } - /** - * For every `xLOAD n`, find all local variable slots that are aliases of `n` using an - * AliasingAnalyzer and change the instruction to `xLOAD m` where `m` is the smallest alias. - * This leaves behind potentially stale `xSTORE n` instructions, which are then eliminated - * by [[eliminateStaleStores]]. - */ - def copyProp(method: MethodNode, owner: InternalName): Boolean = { - AsmAnalyzer.sizeOKForAliasing(method) && { - var changed = false - val numParams = parametersSize(method) - lazy val aliasAnalysis = new AsmAnalyzer(method, owner, new AliasingAnalyzer(new BasicInterpreter)) - - // Remember locals that are used in a `LOAD` instruction. Assume a program has two LOADs: - // - // ... - // LOAD 3 // aliases of 3 here: <3> - // ... - // LOAD 1 // aliases of 1 here: <1, 3> - // - // In this example, we should change the second load from 1 to 3, which might render the - // local variable 1 unused. - val knownUsed = new Array[Boolean](method.maxLocals) - - def usedOrMinAlias(it: IntIterator, init: Int): Int = { - if (knownUsed(init)) init - else { - var r = init - while (it.hasNext) { - val n = it.next() - // knownUsed.lenght is the number of locals, `n` may be a stack slot - if (n < knownUsed.length && knownUsed(n)) return n - if (n < r) r = n - } - r - } - } - - val it = method.instructions.iterator - while (it.hasNext) it.next() match { - case vi: VarInsnNode if vi.`var` >= numParams && isLoad(vi) => - val aliases = aliasAnalysis.frameAt(vi).asInstanceOf[AliasingFrame[_]].aliasesOf(vi.`var`) - if (aliases.size > 1) { - val alias = usedOrMinAlias(aliases.iterator, vi.`var`) - if (alias != -1) { - changed = true - vi.`var` = alias - } - } - knownUsed(vi.`var`) = true - - case _ => - } - - changed - } - } - - /** - * Eliminate `xSTORE` instructions that have no consumer. If the instruction can be completely - * eliminated, it is replaced by a POP. The [[eliminatePushPop]] cleans up unnecessary POPs. - * - * Note that an `ASOTRE` can not always be eliminated: it removes a reference to the object that - * is currently stored in that local, which potentially frees it for GC (SI-5313). Therefore - * we replace such stores by `POP; ACONST_NULL; ASTORE x`. - */ - def eliminateStaleStores(method: MethodNode, owner: InternalName): Boolean = { - AsmAnalyzer.sizeOKForSourceValue(method) && { - lazy val prodCons = new ProdConsAnalyzer(method, owner) - def hasNoCons(varIns: AbstractInsnNode, slot: Int) = prodCons.consumersOfValueAt(varIns.getNext, slot).isEmpty - - // insns to delete: IINC that have no consumer - val toDelete = mutable.ArrayBuffer.empty[IincInsnNode] - - // xSTORE insns to be replaced by POP or POP2 - val storesToDrop = mutable.ArrayBuffer.empty[VarInsnNode] - - // ASTORE insn that have no consumer. - // - if the local is not live, the store is replaced by POP - // - otherwise, pop the argument value and store NULL instead. Unless the boolean field is - // `true`: then the store argument is already known to be ACONST_NULL. - val toNullOut = mutable.ArrayBuffer.empty[(VarInsnNode, Boolean)] - - // `true` for variables that are known to be live - val liveVars = new Array[Boolean](method.maxLocals) - - val it = method.instructions.iterator - while (it.hasNext) it.next() match { - case vi: VarInsnNode if isStore(vi) && hasNoCons(vi, vi.`var`) => - val canElim = vi.getOpcode != ASTORE || { - val currentFieldValueProds = prodCons.initialProducersForValueAt(vi, vi.`var`) - currentFieldValueProds.size == 1 && (currentFieldValueProds.head match { - case ParameterProducer(0) => !isStaticMethod(method) // current field value is `this`, which won't be gc'd anyway - case _: UninitializedLocalProducer => true // field is not yet initialized, so current value cannot leak - case _ => false - }) - } - if (canElim) storesToDrop += vi - else { - val prods = prodCons.producersForValueAt(vi, prodCons.frameAt(vi).stackTop) - val isStoreNull = prods.size == 1 && prods.head.getOpcode == ACONST_NULL - toNullOut += ((vi, isStoreNull)) - } - - case ii: IincInsnNode if hasNoCons(ii, ii.`var`) => - toDelete += ii - - case vi: VarInsnNode => - liveVars(vi.`var`) = true - - case ii: IincInsnNode => - liveVars(ii.`var`) = true - - case _ => - } - - def replaceByPop(vi: VarInsnNode): Unit = { - val size = if (isSize2LoadOrStore(vi.getOpcode)) 2 else 1 - method.instructions.set(vi, getPop(size)) - } - - toDelete foreach method.instructions.remove - - storesToDrop foreach replaceByPop - - for ((vi, isStoreNull) <- toNullOut) { - if (!liveVars(vi.`var`)) replaceByPop(vi) // can drop `ASTORE x` where x has only dead stores - else { - if (!isStoreNull) { - val prev = vi.getPrevious - method.instructions.insert(prev, new InsnNode(ACONST_NULL)) - method.instructions.insert(prev, getPop(1)) - } - } - } - - toDelete.nonEmpty || storesToDrop.nonEmpty || toNullOut.nonEmpty - } - } - - /** - * When a POP instruction has a single producer, remove the POP and eliminate the producer by - * bubbling up the POPs. For example, given - * ILOAD 1; ILOAD 2; IADD; POP - * we first eliminate the POP, then the IADD, then its inputs, so the entire sequence goes away. - * If a producer cannot be eliminated (need to keep side-effects), a POP is inserted. - * - * A special case eliminates the creation of unused objects with side-effect-free constructors: - * NEW scala/Tuple1; DUP; ALOAD 0; INVOKESPECIAL scala/Tuple1.; POP - * The POP has a signle producer (the DUP), it's easy to eliminate these two. A special case - * is needed to eliminate the INVOKESPECIAL and NEW. - */ - def eliminatePushPop(method: MethodNode, owner: InternalName): Boolean = { - AsmAnalyzer.sizeOKForSourceValue(method) && { - // A queue of instructions producing a value that has to be eliminated. If possible, the - // instruction (and its inputs) will be removed, otherwise a POP is inserted after - val queue = mutable.Queue.empty[ProducedValue] - // Contains constructor invocations for values that can be eliminated if unused. - val sideEffectFreeConstructorCalls = mutable.ArrayBuffer.empty[MethodInsnNode] - - // instructions to remove (we don't change the bytecode while analyzing it. this allows - // running the ProdConsAnalyzer only once.) - val toRemove = mutable.Set.empty[AbstractInsnNode] - // instructions to insert before some instruction - val toInsertBefore = mutable.Map.empty[AbstractInsnNode, List[InsnNode]] - // an instruction to insert after some instruction - val toInsertAfter = mutable.Map.empty[AbstractInsnNode, AbstractInsnNode] - - lazy val prodCons = new ProdConsAnalyzer(method, owner) - - /** - * Returns the producers for the stack value `inputSlot` consumed by `cons`, if the consumer - * instruction is the only consumer for all of these producers. - * - * If a producer has multiple consumers, or the value is the caught exception in a catch - * block, this method returns Set.empty. - */ - def producersIfSingleConsumer(cons: AbstractInsnNode, inputSlot: Int): Set[AbstractInsnNode] = { - /** - * True if the values produced by `prod` are all the same. Most instructions produce a single - * value. DUP and DUP2 (with a size-2 input) produce two equivalent values. However, there - * are some exotic instructions that produce multiple non-equal values (DUP_X1, SWAP, ...). - * - * Assume we have `DUP_X2; POP`. In order to remove the `POP` we need to change the DUP_X2 - * into something else, which is not straightforward. - * - * Since scalac never emits any of those exotic bytecodes, we don't optimize them. - */ - def producerHasSingleOutput(prod: AbstractInsnNode): Boolean = prod match { - case _: ExceptionProducer[_] | _: UninitializedLocalProducer => - // POP of an exception in a catch block cannot be removed. For an uninitialized local, - // there should not be a consumer. We are conservative and include it here, so the - // producer would not be removed. - false - - case _: ParameterProducer => - true - - case _ => (prod.getOpcode: @switch) match { - case DUP => true - case DUP2 => prodCons.frameAt(prod).peekStack(0).getSize == 2 - case _ => InstructionStackEffect.prod(InstructionStackEffect.forAsmAnalysis(prod, prodCons.frameAt(prod))) == 1 - } - } - - val prods = prodCons.producersForValueAt(cons, inputSlot) - val singleConsumer = prods forall { prod => - producerHasSingleOutput(prod) && { - // for DUP / DUP2, we only consider the value that is actually consumed by cons - val conss = prodCons.consumersOfValueAt(prod.getNext, inputSlot) - conss.size == 1 && conss.head == cons - } - } - if (singleConsumer) prods else Set.empty - } - - /** - * For a POP instruction that is the single consumer of its producers, remove the POP and - * enqueue the producers. - */ - def handleInitialPop(pop: AbstractInsnNode): Unit = { - val prods = producersIfSingleConsumer(pop, prodCons.frameAt(pop).stackTop) - if (prods.nonEmpty) { - toRemove += pop - val size = if (pop.getOpcode == POP2) 2 else 1 - queue ++= prods.map(ProducedValue(_, size)) - } - } - - /** - * Traverse the method in its initial state and collect all POP instructions and side-effect - * free constructor invocations that can be eliminated. - */ - def collectInitialPopsAndPureConstrs(): Unit = { - val it = method.instructions.iterator - while (it.hasNext) { - val insn = it.next() - (insn.getOpcode: @switch) match { - case POP | POP2 => - handleInitialPop(insn) - - case INVOKESPECIAL => - val mi = insn.asInstanceOf[MethodInsnNode] - if (isSideEffectFreeConstructorCall(mi)) sideEffectFreeConstructorCalls += mi - - case _ => - } - } - } - - /** - * Eliminate the `numArgs` inputs of the instruction `prod` (which was eliminated). Fo - * each input value - * - if the `prod` instruction is the single consumer, enqueue the producers of the input - * - otherwise, insert a POP instruction to POP the input value - */ - def handleInputs(prod: AbstractInsnNode, numArgs: Int): Unit = { - val frame = prodCons.frameAt(prod) - val pops = mutable.ListBuffer.empty[InsnNode] - @tailrec def handle(stackOffset: Int): Unit = { - if (stackOffset >= 0) { - val prods = producersIfSingleConsumer(prod, frame.stackTop - stackOffset) - val nSize = frame.peekStack(stackOffset).getSize - if (prods.isEmpty) pops append getPop(nSize) - else queue ++= prods.map(ProducedValue(_, nSize)) - handle(stackOffset - 1) - } - } - handle(numArgs - 1) // handle stack offsets (numArgs - 1) to 0 - if (pops.nonEmpty) toInsertBefore(prod) = pops.toList - } - - /** - * Eliminate the closure value produced by `indy`. If the SAM type is known to construct - * without side-effects (e.g. scala/runtime/java8/JFunctionN), the `indy` and its inputs - * are eliminated, otherwise a POP is inserted. - */ - def handleClosureInst(indy: InvokeDynamicInsnNode): Unit = { - if (isrJFunctionType(Type.getReturnType(indy.desc).getInternalName)) { - toRemove += indy - callGraph.removeClosureInstantiation(indy, method) - handleInputs(indy, Type.getArgumentTypes(indy.desc).length) - } else { - toInsertAfter(indy) = getPop(1) - } - } - - def runQueue(): Unit = while (queue.nonEmpty) { - val ProducedValue(prod, size) = queue.dequeue() - - def prodString = s"Producer ${AsmUtils textify prod}@${method.instructions.indexOf(prod)}\n${AsmUtils textify method}" - def popAfterProd(): Unit = toInsertAfter(prod) = getPop(size) - - (prod.getOpcode: @switch) match { - case ACONST_NULL | ICONST_M1 | ICONST_0 | ICONST_1 | ICONST_2 | ICONST_3 | ICONST_4 | ICONST_5 | LCONST_0 | LCONST_1 | FCONST_0 | FCONST_1 | FCONST_2 | DCONST_0 | DCONST_1 | - BIPUSH | SIPUSH | ILOAD | LLOAD | FLOAD | DLOAD | ALOAD=> - toRemove += prod - - case opc @ (DUP | DUP2) => - assert(opc != 2 || size == 2, s"DUP2 for two size-1 values; $prodString") // ensured in method `producerHasSingleOutput` - if (toRemove(prod)) - // the DUP is already scheduled for removal because one of its consumers is a POP. - // now the second consumer is also a POP, so we need to eliminate the DUP's input. - handleInputs(prod, 1) - else - toRemove += prod - - case DUP_X1 | DUP_X2 | DUP2_X1 | DUP2_X2 | SWAP => - // these are excluded in method `producerHasSingleOutput` - assert(false, s"Cannot eliminate value pushed by an instruction with multiple output values; $prodString") - - case IDIV | LDIV | IREM | LREM => - popAfterProd() // keep potential division by zero - - case IADD | LADD | FADD | DADD | ISUB | LSUB | FSUB | DSUB | IMUL | LMUL | FMUL | DMUL | FDIV | DDIV | FREM | DREM | - LSHL | LSHR | LUSHR | - IAND | IOR | IXOR | LAND | LOR | LXOR | - LCMP | FCMPL | FCMPG | DCMPL | DCMPG => - toRemove += prod - handleInputs(prod, 2) - - case INEG | LNEG | FNEG | DNEG | - I2L | I2F | I2D | L2I | L2F | L2D | F2I | F2L | F2D | D2I | D2L | D2F | I2B | I2C | I2S => - toRemove += prod - handleInputs(prod, 1) - - case GETFIELD | GETSTATIC => - // TODO eliminate side-effect free module loads (https://github.com/scala/scala-dev/issues/16) - if (isBoxedUnit(prod)) toRemove += prod - else popAfterProd() // keep potential class initialization (static field) or NPE (instance field) - - case INVOKEVIRTUAL | INVOKESPECIAL | INVOKESTATIC | INVOKEINTERFACE => - val methodInsn = prod.asInstanceOf[MethodInsnNode] - if (isSideEffectFreeCall(methodInsn)) { - toRemove += prod - callGraph.removeCallsite(methodInsn, method) - val receiver = if (methodInsn.getOpcode == INVOKESTATIC) 0 else 1 - handleInputs(prod, Type.getArgumentTypes(methodInsn.desc).length + receiver) - } else - popAfterProd() - - case INVOKEDYNAMIC => - prod match { - case callGraph.LambdaMetaFactoryCall(indy, _, _, _) => handleClosureInst(indy) - case _ => popAfterProd() - } - - case NEW => - if (isNewForSideEffectFreeConstructor(prod)) toRemove += prod - else popAfterProd() - - case LDC => prod.asInstanceOf[LdcInsnNode].cst match { - case _: java.lang.Integer | _: java.lang.Float | _: java.lang.Long | _: java.lang.Double | _: String => - toRemove += prod - - case _ => - // don't remove class literals, method types, method handles: keep a potential NoClassDefFoundError - popAfterProd() - } - - case NEWARRAY | ANEWARRAY => - toRemove += prod - handleInputs(prod, 1) - - case MULTIANEWARRAY => - toRemove += prod - handleInputs(prod, prod.asInstanceOf[MultiANewArrayInsnNode].dims) - - case _ => - popAfterProd() - } - } - - // there are two cases when we can eliminate a constructor call: - // - NEW T; INVOKESPECIAL T. -- there's no DUP, the new object is consumed only by the constructor) - // - NEW T; DUP; INVOKESPECIAL T., where the DUP will be removed - def eliminateUnusedPureConstructorCalls(): Boolean = { - var changed = false - - def removeConstructorCall(mi: MethodInsnNode): Unit = { - toRemove += mi - callGraph.removeCallsite(mi, method) - sideEffectFreeConstructorCalls -= mi - changed = true - } - - for (mi <- sideEffectFreeConstructorCalls.toList) { // toList to allow removing elements while traversing - val frame = prodCons.frameAt(mi) - val stackTop = frame.stackTop - val numArgs = Type.getArgumentTypes(mi.desc).length - val receiverProds = producersIfSingleConsumer(mi, stackTop - numArgs) - if (receiverProds.size == 1) { - val receiverProd = receiverProds.head - if (receiverProd.getOpcode == NEW) { - removeConstructorCall(mi) - handleInputs(mi, numArgs + 1) // removes the producers of args and receiver - } else if (receiverProd.getOpcode == DUP && toRemove.contains(receiverProd)) { - val dupProds = producersIfSingleConsumer(receiverProd, prodCons.frameAt(receiverProd).stackTop) - if (dupProds.size == 1 && dupProds.head.getOpcode == NEW) { - removeConstructorCall(mi) - handleInputs(mi, numArgs) // removes the producers of args. the producer of the receiver is DUP and already in toRemove. - queue += ProducedValue(dupProds.head, 1) // removes the NEW (which is NOT the producer of the receiver!) - } - } - } - } - changed - } - - collectInitialPopsAndPureConstrs() - - // eliminating producers enables eliminating unused constructor calls (when a DUP gets removed). - // vice-versa, eliminating a constructor call adds producers of constructor parameters to the queue. - // so the two run in a loop. - runQueue() - while (eliminateUnusedPureConstructorCalls()) - runQueue() - - var changed = false - toInsertAfter foreach { - case (target, insn) => - nextExecutableInstructionOrLabel(target) match { - // `insn` is of type `InsnNode`, so we only need to check the Opcode when comparing to another instruction - case Some(next) if next.getOpcode == insn.getOpcode && toRemove(next) => - // Inserting and removing a POP at the same place should not enable `changed`. This happens - // when a POP directly follows a producer that cannot be eliminated, e.g. INVOKESTATIC A.m ()I; POP - // The POP is initially added to `toRemove`, and the `INVOKESTATIC` producer is added to the queue. - // Because the producer cannot be elided, a POP is added to `toInsertAfter`. - toRemove -= next - - case _ => - changed = true - method.instructions.insert(target, insn) - } - } - toInsertBefore foreach { - case (target, insns) => - changed = true - insns.foreach(method.instructions.insertBefore(target, _)) - } - toRemove foreach { insn => - changed = true - method.instructions.remove(insn) - } - changed - } - } - - case class ProducedValue(producer: AbstractInsnNode, size: Int) { - override def toString = s"<${AsmUtils textify producer}>" - } - /** * Eliminate `CHECKCAST` instructions that are statically known to succeed. This is safe if the * tested object is null: `null.asInstanceOf` always succeeds. @@ -985,176 +537,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { toRemove.nonEmpty } } - - /** - * Remove `xSTORE n; xLOAD n` paris if - * - the local variable n is not used anywhere else in the method (1), and - * - there are no executable instructions and no live labels (jump targets) between the two (2) - * - * Note: store-load pairs that cannot be eliminated could be replaced by `DUP; xSTORE n`, but - * that's just cosmetic and doesn't help for anything. - * - * (1) This could be made more precise by running a prodCons analysis and checking that the load - * is the only user of the store. Then we could eliminate the pair even if the variable is live - * (except for ASTORE, SI-5313). Not needing an analyzer is more efficient, and catches most - * cases. - * - * (2) The implementation uses a conservative estimation for liveness (if some instruction uses - * local n, then n is considered live in the entire method). In return, it doesn't need to run an - * Analyzer on the method, making it more efficient. - * - * This method also removes `ACONST_NULL; ASTORE n` if the local n is not live. This pattern is - * introduced by [[eliminateStaleStores]]. - * - * The implementation is a little tricky to support the following case: - * ISTORE 1; ISTORE 2; ILOAD 2; ACONST_NULL; ASTORE 3; ILOAD 1 - * The outer store-load pair can be removed if two the inner pairs can be. - */ - def eliminateStoreLoad(method: MethodNode): Boolean = { - val removePairs = mutable.Set.empty[RemovePair] - val liveVars = new Array[Boolean](method.maxLocals) - val liveLabels = mutable.Set.empty[LabelNode] - - def mkRemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]): RemovePair = { - val r = RemovePair(store, other, depends) - removePairs += r - r - } - - def registerLiveVarsLabels(insn: AbstractInsnNode): Unit = insn match { - case vi: VarInsnNode => liveVars(vi.`var`) = true - case ii: IincInsnNode => liveVars(ii.`var`) = true - case j: JumpInsnNode => liveLabels += j.label - case s: TableSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala - case s: LookupSwitchInsnNode => liveLabels += s.dflt; liveLabels ++= s.labels.asScala - case _ => - } - - val pairStartStack = new mutable.Stack[(AbstractInsnNode, mutable.ListBuffer[RemovePairDependency])] - - def push(insn: AbstractInsnNode) = { - pairStartStack push ((insn, mutable.ListBuffer.empty)) - } - - def addDepends(dependency: RemovePairDependency) = if (pairStartStack.nonEmpty) { - val (_, depends) = pairStartStack.top - depends += dependency - } - - def completesStackTop(load: AbstractInsnNode) = isLoad(load) && pairStartStack.nonEmpty && { - pairStartStack.top match { - case (store: VarInsnNode, _) => store.`var` == load.asInstanceOf[VarInsnNode].`var` - case _ => false - } - } - - /** - * Try to pair `insn` with its correspondant on the stack - * - if the stack top is a store and `insn` is a corresponding load, create a pair - * - otherwise, check the two top stack values for `null; store`. if it matches, create - * a pair and continue pairing `insn` on the remaining stack - * - otherwise, empty the stack and mark the local variables in it live - */ - def tryToPairInstruction(insn: AbstractInsnNode): Unit = { - @tailrec def emptyStack(): Unit = if (pairStartStack.nonEmpty) { - registerLiveVarsLabels(pairStartStack.pop()._1) - emptyStack() - } - - @tailrec def tryPairing(): Unit = { - if (completesStackTop(insn)) { - val (store: VarInsnNode, depends) = pairStartStack.pop() - addDepends(mkRemovePair(store, insn, depends.toList)) - } else if (pairStartStack.nonEmpty) { - val (top, topDepends) = pairStartStack.pop() - if (pairStartStack.nonEmpty) { - (pairStartStack.top, top) match { - case ((ldNull: InsnNode, depends), store: VarInsnNode) if ldNull.getOpcode == ACONST_NULL && store.getOpcode == ASTORE => - pairStartStack.pop() - addDepends(mkRemovePair(store, ldNull, depends.toList)) - // example: store; (null; store;) (store; load;) load - // s1^ ^^^^^p1^^^^^ // p1 is added to s1's depends - // then: store; (null; store;) load - // s2^ ^^^^p2^^^^^ // p1 and p2 are added to s2's depends - topDepends foreach addDepends - tryPairing() - - case _ => - // empty the stack - a non-matching insn was found, cannot create any pairs to remove - registerLiveVarsLabels(insn) - registerLiveVarsLabels(top) - emptyStack() - } - } else { - // stack only has one element - registerLiveVarsLabels(insn) - registerLiveVarsLabels(top) - } - } else { - // stack is empty already - registerLiveVarsLabels(insn) - } - } - - tryPairing() - } - - - var insn = method.instructions.getFirst - - @tailrec def advanceToNextExecutableOrLabel(): Unit = { - insn = insn.getNext - if (insn != null && !isExecutable(insn) && !insn.isInstanceOf[LabelNode]) advanceToNextExecutableOrLabel() - } - - while (insn != null) { - insn match { - case _ if insn.getOpcode == ACONST_NULL => push(insn) - case vi: VarInsnNode if isStore(vi) => push(insn) - case label: LabelNode if pairStartStack.nonEmpty => addDepends(LabelNotLive(label)) - case _ => tryToPairInstruction(insn) - } - advanceToNextExecutableOrLabel() - } - - // elide RemovePairs that depend on live labels or other RemovePair that have to be elided. - // example: store 1; store 2; label x; load 2; load 1 - // if x is live, the inner pair has to be elided, causing the outer pair to be elided too. - - var doneEliding = false - - def elide(removePair: RemovePair) = { - doneEliding = false - liveVars(removePair.store.`var`) = true - removePairs -= removePair - } - - while (!doneEliding) { - doneEliding = true - for (removePair <- removePairs.toList) { - val slot = removePair.store.`var` - if (liveVars(slot)) elide(removePair) - else removePair.depends foreach { - case LabelNotLive(label) => if (liveLabels(label)) elide(removePair) - case other: RemovePair => if (!removePairs(other)) elide(removePair) - } - } - } - - for (removePair <- removePairs) { - method.instructions.remove(removePair.store) - method.instructions.remove(removePair.other) - } - - removePairs.nonEmpty - } -} - -trait RemovePairDependency -case class RemovePair(store: VarInsnNode, other: AbstractInsnNode, depends: List[RemovePairDependency]) extends RemovePairDependency { - override def toString = s"<${AsmUtils textify store},${AsmUtils textify other}> [$depends]" } -case class LabelNotLive(label: LabelNode) extends RemovePairDependency object LocalOptImpls { /** -- cgit v1.2.3 From 4eee5e088eb72ba00aa4b02a03a1bcf27b5d751b Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Wed, 16 Dec 2015 14:05:37 +0100 Subject: Run DCE before the closure optimizer (fixes a crash) Before identifying function callsites within the same method as a closure allocation, run DCE. The ProdCons analysis used to identify these function calls may crash if there is unreachable code, as observed in the community build with scala-js. The crash was rare because inlining, which is performed before closure optimizations, already runs DCE. However, inlining may render more code unreachable (e.g. when inlining a method that throws). Also make sure that DCE is always performed on the callee before inlining: move the DCE invocation into the inlineCallsite method, which is also invoked by the closure optimizer. --- .../tools/nsc/backend/jvm/BackendReporting.scala | 2 +- .../nsc/backend/jvm/opt/ClosureOptimizer.scala | 75 +++++---- .../scala/tools/nsc/backend/jvm/opt/Inliner.scala | 169 +++++++++++---------- .../nsc/backend/jvm/opt/ClosureOptimizerTest.scala | 19 +++ 4 files changed, 154 insertions(+), 111 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala index 05cc484135..13f4738d3d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BackendReporting.scala @@ -287,7 +287,7 @@ object BackendReporting { s"Failed to get the type of a method of class symbol $classFullName due to SI-9111." case ClassNotFoundWhenBuildingInlineInfoFromSymbol(missingClass) => - s"Failed to build the inline information: $missingClass." + s"Failed to build the inline information: $missingClass" case UnknownScalaInlineInfoVersion(internalName, version) => s"Cannot read ScalaInlineInfo version $version in classfile $internalName. Use a more recent compiler." diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala index 61d10eeae9..a041af2621 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala @@ -8,7 +8,7 @@ package backend.jvm package opt import scala.annotation.switch -import scala.collection.immutable +import scala.collection.{mutable, immutable} import scala.collection.immutable.IntMap import scala.reflect.internal.util.NoPosition import scala.tools.asm.{Handle, Type, Opcodes} @@ -73,37 +73,58 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { } } - // For each closure instantiation, a list of callsites of the closure that can be re-written - // If a callsite cannot be rewritten, for example because the lambda body method is not accessible, - // a warning is returned instead. - val callsitesToRewrite: List[(ClosureInstantiation, List[Either[RewriteClosureApplyToClosureBodyFailed, (MethodInsnNode, Int)]])] = { - closureInstantiations.iterator.flatMap({ - case (methodNode, closureInits) => - if (!AsmAnalyzer.sizeOKForSourceValue(methodNode)) Nil else { + // Use collections that keep insertion order, ensures order of closure rewrites (bytecode stability) + val toRewrite = mutable.LinkedHashMap.empty[ClosureInstantiation, mutable.ArrayBuffer[(MethodInsnNode, Int)]] + def addRewrite(init: ClosureInstantiation, invocation: MethodInsnNode, stackHeight: Int) = { + val calls = toRewrite.getOrElseUpdate(init, mutable.ArrayBuffer.empty[(MethodInsnNode, Int)]) + calls += ((invocation, stackHeight)) + } + + // For each closure instantiation find callsites of the closure and add them to the toRewrite + // buffer (cannot change a method's bytecode while still looking for further invocations to + // rewrite, the frame indices of the ProdCons analysis would get out of date). If a callsite + // cannot be rewritten, for example because the lambda body method is not accessible, issue a + // warning. The `toList` in the next line prevents modifying closureInstantiations while + // iterating it: minimalRemoveUnreachableCode (called in the loop) removes elements. + for (method <- closureInstantiations.keysIterator.toList if AsmAnalyzer.sizeOKForBasicValue(method)) closureInstantiations.get(method) match { + case Some(closureInitsBeforeDCE) if closureInitsBeforeDCE.nonEmpty => + val ownerClass = closureInitsBeforeDCE.head._2.ownerClass.internalName + + // Advanced ProdCons queries (initialProducersForValueAt) expect no unreachable code. + localOpt.minimalRemoveUnreachableCode(method, ownerClass) + + if (AsmAnalyzer.sizeOKForSourceValue(method)) closureInstantiations.get(method) match { + case Some(closureInits) => // A lazy val to ensure the analysis only runs if necessary (the value is passed by name to `closureCallsites`) - // We don't need to worry about the method being too large for running an analysis: large - // methods are not added to the call graph / closureInstantiations map. - lazy val prodCons = new ProdConsAnalyzer(methodNode, closureInits.valuesIterator.next().ownerClass.internalName) + lazy val prodCons = new ProdConsAnalyzer(method, ownerClass) + // sorting for bytecode stability (e.g. indices of local vars created during the rewrite) val sortedInits = immutable.TreeSet.empty ++ closureInits.values - sortedInits.iterator.map(init => (init, closureCallsites(init, prodCons))).filter(_._2.nonEmpty) - } - }).toList // mapping to a list (not a map) to keep the sorting + + for (init <- sortedInits) { + val callsites = closureCallsites(init, prodCons) + if (callsites.nonEmpty) { + callsites foreach { + case Left(warning) => + backendReporting.inlinerWarning(warning.pos, warning.toString) + + case Right((invocation, stackHeight)) => + addRewrite(init, invocation, stackHeight) + } + } + } + + case _ => + } + + case _ => } - // Rewrite all closure callsites (or issue inliner warnings for those that cannot be rewritten) - for ((closureInit, callsites) <- callsitesToRewrite) { - // Local variables that hold the captured values and the closure invocation arguments. - // They are lazy vals to ensure that locals for captured values are only allocated if there's - // actually a callsite to rewrite (an not only warnings to be issued). - lazy val (localsForCapturedValues, argumentLocalsList) = localsForClosureRewrite(closureInit) - for (callsite <- callsites) callsite match { - case Left(warning) => - backendReporting.inlinerWarning(warning.pos, warning.toString) - - case Right((invocation, stackHeight)) => - rewriteClosureApplyInvocation(closureInit, invocation, stackHeight, localsForCapturedValues, argumentLocalsList) - } + toRewrite foreach { + case (closureInit, invocations) => + // Local variables that hold the captured values and the closure invocation arguments. + val (localsForCapturedValues, argumentLocalsList) = localsForClosureRewrite(closureInit) + for ((invocation, stackHeight) <- invocations) rewriteClosureApplyInvocation(closureInit, invocation, stackHeight, localsForCapturedValues, argumentLocalsList) } } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala index d1bccf614e..f247e884ae 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/Inliner.scala @@ -70,11 +70,13 @@ class Inliner[BT <: BTypes](val btypes: BT) { } } + // Rewriting final trait method callsites to the implementation class enables inlining. def rewriteFinalTraitMethodInvocations(): Unit = { - // Rewriting final trait method callsites to the implementation class enables inlining. - // We cannot just iterate over the values of the `callsites` map because the rewrite changes the - // map. Therefore we first copy the values to a list. - callsites.valuesIterator.flatMap(_.valuesIterator).toList.foreach(rewriteFinalTraitMethodInvocation) + // Collect callsites to rewrite before actually rewriting anything. This prevents changing the + // `callsties` map while iterating it. + val toRewrite = mutable.ArrayBuffer.empty[Callsite] + for (css <- callsites.valuesIterator; cs <- css.valuesIterator if doRewriteTraitCallsite(cs)) toRewrite += cs + toRewrite foreach rewriteFinalTraitMethodInvocation } /** @@ -93,78 +95,82 @@ class Inliner[BT <: BTypes](val btypes: BT) { * (the receiver type is the interface, so the method is abstract). */ def rewriteFinalTraitMethodInvocation(callsite: Callsite): Unit = { - if (doRewriteTraitCallsite(callsite)) { - val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, samParamTypes, infoWarning)) = callsite.callee + // The analyzer used below needs to have a non-null frame for the callsite instruction + localOpt.minimalRemoveUnreachableCode(callsite.callsiteMethod, callsite.callsiteClass.internalName) - val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) + // If the callsite was eliminated by DCE, do nothing. + if (!callGraph.containsCallsite(callsite)) return - val implClassInternalName = calleeDeclarationClass.internalName + "$class" + val Right(Callee(callee, calleeDeclarationClass, _, _, annotatedInline, annotatedNoInline, samParamTypes, infoWarning)) = callsite.callee - val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match { - case Some(internalName) => classBTypeFromParsedClassfile(internalName) - case None => calleeDeclarationClass - }) + val traitMethodArgumentTypes = asm.Type.getArgumentTypes(callee.desc) - def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = { - byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1) - } + val implClassInternalName = calleeDeclarationClass.internalName + "$class" - // The rewrite reading the implementation class and the implementation method from the bytecode - // repository. If either of the two fails, the rewrite is not performed. - val res = for { - selfParamType <- selfParamTypeV - implMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*) - implClassMethod <- implClassMethodV(implMethodDescriptor) - implClassBType = classBTypeFromParsedClassfile(implClassInternalName) - selfTypeOk <- calleeDeclarationClass.isSubtypeOf(selfParamType) - } yield { - - // The self parameter type may be incompatible with the trait type. - // trait T { self: S => def foo = 1 } - // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write - // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a - // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the - // receiver value and add a cast to the self type after each. - if (!selfTypeOk) { - // We don't need to worry about the method being too large for running an analysis. - // Callsites of large methods are not added to the call graph. - val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new Analyzer(new SourceInterpreter)) - val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekStack(traitMethodArgumentTypes.length) - for (i <- receiverValue.insns.asScala) { - val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName) - callsite.callsiteMethod.instructions.insert(i, cast) - } - } + val selfParamTypeV: Either[OptimizerWarning, ClassBType] = calleeDeclarationClass.info.map(_.inlineInfo.traitImplClassSelfType match { + case Some(internalName) => classBTypeFromParsedClassfile(internalName) + case None => calleeDeclarationClass + }) - val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false) - callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) - callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) + def implClassMethodV(implMethodDescriptor: String): Either[OptimizerWarning, MethodNode] = { + byteCodeRepository.methodNode(implClassInternalName, callee.name, implMethodDescriptor).map(_._1) + } - callGraph.removeCallsite(callsite.callsiteInstruction, callsite.callsiteMethod) - val staticCallSamParamTypes = { - if (selfParamType.info.get.inlineInfo.sam.isEmpty) samParamTypes - 0 - else samParamTypes.updated(0, selfParamType) + // The rewrite reading the implementation class and the implementation method from the bytecode + // repository. If either of the two fails, the rewrite is not performed. + val res = for { + selfParamType <- selfParamTypeV + implMethodDescriptor = asm.Type.getMethodDescriptor(asm.Type.getReturnType(callee.desc), selfParamType.toASMType +: traitMethodArgumentTypes: _*) + implClassMethod <- implClassMethodV(implMethodDescriptor) + implClassBType = classBTypeFromParsedClassfile(implClassInternalName) + selfTypeOk <- calleeDeclarationClass.isSubtypeOf(selfParamType) + } yield { + + // The self parameter type may be incompatible with the trait type. + // trait T { self: S => def foo = 1 } + // The $self parameter type of T$class.foo is S, which may be unrelated to T. If we re-write + // a call to T.foo to T$class.foo, we need to cast the receiver to S, otherwise we get a + // VerifyError. We run a `SourceInterpreter` to find all producer instructions of the + // receiver value and add a cast to the self type after each. + if (!selfTypeOk) { + // We don't need to worry about the method being too large for running an analysis. + // Callsites of large methods are not added to the call graph. + val analyzer = new AsmAnalyzer(callsite.callsiteMethod, callsite.callsiteClass.internalName, new Analyzer(new SourceInterpreter)) + val receiverValue = analyzer.frameAt(callsite.callsiteInstruction).peekStack(traitMethodArgumentTypes.length) + for (i <- receiverValue.insns.asScala) { + val cast = new TypeInsnNode(CHECKCAST, selfParamType.internalName) + callsite.callsiteMethod.instructions.insert(i, cast) } - val staticCallsite = callsite.copy( - callsiteInstruction = newCallsiteInstruction, - callee = Right(Callee( - callee = implClassMethod, - calleeDeclarationClass = implClassBType, - safeToInline = true, - safeToRewrite = false, - annotatedInline = annotatedInline, - annotatedNoInline = annotatedNoInline, - samParamTypes = staticCallSamParamTypes, - calleeInfoWarning = infoWarning)) - ) - callGraph.addCallsite(staticCallsite) } - for (warning <- res.left) { - val Right(callee) = callsite.callee - val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning))) - callGraph.addCallsite(callsite.copy(callee = Right(newCallee))) + val newCallsiteInstruction = new MethodInsnNode(INVOKESTATIC, implClassInternalName, callee.name, implMethodDescriptor, false) + callsite.callsiteMethod.instructions.insert(callsite.callsiteInstruction, newCallsiteInstruction) + callsite.callsiteMethod.instructions.remove(callsite.callsiteInstruction) + + callGraph.removeCallsite(callsite.callsiteInstruction, callsite.callsiteMethod) + val staticCallSamParamTypes = { + if (selfParamType.info.get.inlineInfo.sam.isEmpty) samParamTypes - 0 + else samParamTypes.updated(0, selfParamType) } + val staticCallsite = callsite.copy( + callsiteInstruction = newCallsiteInstruction, + callee = Right(Callee( + callee = implClassMethod, + calleeDeclarationClass = implClassBType, + safeToInline = true, + safeToRewrite = false, + annotatedInline = annotatedInline, + annotatedNoInline = annotatedNoInline, + samParamTypes = staticCallSamParamTypes, + calleeInfoWarning = infoWarning)) + ) + callGraph.addCallsite(staticCallsite) + } + + for (warning <- res.left) { + val Right(callee) = callsite.callee + val newCallee = callee.copy(calleeInfoWarning = Some(RewriteTraitCallToStaticImplMethodFailed(calleeDeclarationClass.internalName, callee.callee.name, callee.callee.desc, warning))) + callGraph.addCallsite(callsite.copy(callee = Right(newCallee))) } } @@ -302,21 +308,9 @@ class Inliner[BT <: BTypes](val btypes: BT) { def inline(request: InlineRequest): List[CannotInlineWarning] = canInlineBody(request.callsite) match { case Some(w) => List(w) case None => - // Inlining a method can create unreachable code. Example: - // def f = throw e - // def g = f; println() // println is unreachable after inlining f - // If we have an inline request for a call to g, and f has been already inlined into g, we - // need to run DCE before inlining g. - val Right(callee) = request.callsite.callee - localOpt.minimalRemoveUnreachableCode(callee.callee, callee.calleeDeclarationClass.internalName) - // Skip over DCE'd callsites - if (callGraph.containsCallsite(request.callsite)) { - inlineCallsite(request.callsite) - val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite)) - postRequests flatMap inline - } else { - Nil - } + inlineCallsite(request.callsite) + val postRequests = request.post.flatMap(adaptPostRequestForMainCallsite(_, request.callsite)) + postRequests flatMap inline } /** @@ -325,8 +319,6 @@ class Inliner[BT <: BTypes](val btypes: BT) { * Preconditions: * - The callsite can safely be inlined (canInlineBody is true) * - The maxLocals and maxStack values of the callsite method are correctly computed - * - The callsite method contains no unreachable basic blocks, i.e., running an Analyzer does - * not produce any `null` frames * * @return A map associating instruction nodes of the callee with the corresponding cloned * instruction in the callsite method. @@ -336,6 +328,17 @@ class Inliner[BT <: BTypes](val btypes: BT) { val Right(callsiteCallee) = callsite.callee import callsiteCallee.{callee, calleeDeclarationClass} + // Inlining requires the callee not to have unreachable code, the analyzer used below should not + // return any `null` frames. Note that inlining a method can create unreachable code. Example: + // def f = throw e + // def g = f; println() // println is unreachable after inlining f + // If we have an inline request for a call to g, and f has been already inlined into g, we + // need to run DCE on g's body before inlining g. + localOpt.minimalRemoveUnreachableCode(callee, calleeDeclarationClass.internalName) + + // If the callsite was eliminated by DCE, do nothing. + if (!callGraph.containsCallsite(callsite)) return + // New labels for the cloned instructions val labelsMap = cloneLabels(callee) val (clonedInstructions, instructionMap, hasSerializableClosureInstantiation) = cloneInstructions(callee, labelsMap) @@ -512,7 +515,7 @@ class Inliner[BT <: BTypes](val btypes: BT) { // Remove the elided invocation from the call graph callGraph.removeCallsite(callsiteInstruction, callsiteMethod) - // Inlining a method body can render some code unreachable, see example above (in runInliner). + // Inlining a method body can render some code unreachable, see example above in this method. unreachableCodeEliminated -= callsiteMethod } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala index 0f87280000..b314643fb1 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala @@ -87,4 +87,23 @@ class ClosureOptimizerTest extends ClearAfterClass { TypeOp(CHECKCAST, "java/lang/String"), Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(Ljava/lang/String;)Ljava/lang/String;", false), Op(ARETURN))) } + + @Test + def closureOptWithUnreachableCode(): Unit = { + // this example used to crash the ProdCons analysis in the closure optimizer - ProdCons + // expects no unreachable code. + val code = + """class C { + | @inline final def m = throw new Error("") + | def t = { + | val f = (x: Int) => x + 1 + | m + | f(10) // unreachable after inlining m + | } + |} + """.stripMargin + val List(c) = compileClasses(compiler)(code) + assertEquals(getSingleMethod(c, "t").instructions.summary, + List(NEW, DUP, LDC, "", ATHROW)) + } } -- cgit v1.2.3 From 7443037a6d1376709b3d83f10e66c9155f76dbaa Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Wed, 20 Jan 2016 13:27:15 +0100 Subject: Precompile some regexes used in the backend --- .../scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala | 4 ++-- src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala | 11 ++++++++--- .../scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala index 739b775e56..baf82032f7 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala @@ -147,10 +147,10 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { def getBoxedUnit: FieldInsnNode = new FieldInsnNode(Opcodes.GETSTATIC, coreBTypes.srBoxedUnitRef.internalName, "UNIT", coreBTypes.srBoxedUnitRef.descriptor) - private val anonfunAdaptedName = """.*\$anonfun\$\d+\$adapted""" + private val anonfunAdaptedName = """.*\$anonfun\$\d+\$adapted""".r def hasAdaptedImplMethod(closureInit: ClosureInstantiation): Boolean = { BytecodeUtils.isrJFunctionType(Type.getReturnType(closureInit.lambdaMetaFactoryCall.indy.desc).getInternalName) && - closureInit.lambdaMetaFactoryCall.implMethod.getName.matches(anonfunAdaptedName) + anonfunAdaptedName.pattern.matcher(closureInit.lambdaMetaFactoryCall.implMethod.getName).matches } /** diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala index a8829b6e29..4ecaf17a10 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala @@ -763,9 +763,14 @@ class BoxUnbox[BT <: BTypes](val btypes: BT) { } } - private def isSpecializedTupleClass(tupleClass: InternalName) = tupleClass matches "scala/Tuple[12]\\$mc[IJDCZ]{1,2}\\$sp" - private def isSpecializedTupleGetter(mi: MethodInsnNode) = mi.name matches "_[12]\\$mc[IJDCZ]\\$sp" - private def isTupleGetter(mi: MethodInsnNode) = mi.name matches "_\\d\\d?" + private val specializedTupleClassR = "scala/Tuple[12]\\$mc[IJDCZ]{1,2}\\$sp".r + private def isSpecializedTupleClass(tupleClass: InternalName) = specializedTupleClassR.pattern.matcher(tupleClass).matches + + private val specializedTupleGetterR = "_[12]\\$mc[IJDCZ]\\$sp".r + private def isSpecializedTupleGetter(mi: MethodInsnNode) = specializedTupleGetterR.pattern.matcher(mi.name)matches + + private val tupleGetterR = "_\\d\\d?".r + private def isTupleGetter(mi: MethodInsnNode) = tupleGetterR.pattern.matcher(mi.name).matches def checkTupleExtraction(insn: AbstractInsnNode, kind: Tuple, prodCons: ProdConsAnalyzer): Option[BoxConsumer] = { val expectedTupleClass = kind.tupleClass diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala index a041af2621..35bc82d098 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala @@ -218,7 +218,7 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { receiverProducers.size == 1 && receiverProducers.head == indy } - def isSpecializedVersion(specName: String, nonSpecName: String) = specName.startsWith(nonSpecName) && specName.substring(nonSpecName.length).matches(specializationSuffix) + def isSpecializedVersion(specName: String, nonSpecName: String) = specName.startsWith(nonSpecName) && specializationSuffix.pattern.matcher(specName.substring(nonSpecName.length)).matches def sameOrSpecializedType(specTp: Type, nonSpecTp: Type) = { specTp == nonSpecTp || { @@ -517,5 +517,5 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { object ClosureOptimizer { val primitives = "BSIJCFDZV" - val specializationSuffix = s"(\\$$mc[$primitives]+\\$$sp)" + val specializationSuffix = s"(\\$$mc[$primitives]+\\$$sp)".r } -- cgit v1.2.3 From 8d3be4dc79cb4679fc4994c32b21e10847e5518f Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 22 Jan 2016 22:33:29 +0100 Subject: Harden methods to recognize method invocations to optimize The previous methods to identify method invocations that can be optimized, such as `isPredefAutoBox`, were String-based. Now we obtain class and method signatures from symbols through the BTypes infrastructure. We also piggy-back on specialization's type transformer to create all specialized subclasses of Tuple1/Tuple2. We'll do the same in the future for FunctionN, but the current JFunctionN are written in Java and specialized artisanally. --- src/compiler/scala/tools/nsc/Global.scala | 2 +- .../tools/nsc/backend/jvm/BCodeBodyBuilder.scala | 4 +- .../scala/tools/nsc/backend/jvm/BTypes.scala | 2 +- .../tools/nsc/backend/jvm/BTypesFromSymbols.scala | 8 +- .../scala/tools/nsc/backend/jvm/CoreBTypes.scala | 273 ++++++++++++++------ .../nsc/backend/jvm/analysis/BackendUtils.scala | 138 +++++++++- .../jvm/analysis/InstructionStackEffect.scala | 1 - .../backend/jvm/analysis/NullnessAnalyzer.scala | 6 +- .../tools/nsc/backend/jvm/opt/BytecodeUtils.scala | 285 --------------------- .../tools/nsc/backend/jvm/opt/CallGraph.scala | 2 +- .../nsc/backend/jvm/opt/ClosureOptimizer.scala | 20 +- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 2 +- .../scala/tools/nsc/transform/LambdaLift.scala | 5 - .../scala/reflect/internal/Definitions.scala | 4 + .../scala/reflect/runtime/JavaUniverseForce.scala | 1 + .../jvm/analysis/NullnessAnalyzerTest.scala | 2 +- 16 files changed, 350 insertions(+), 405 deletions(-) diff --git a/src/compiler/scala/tools/nsc/Global.scala b/src/compiler/scala/tools/nsc/Global.scala index 422e2080f0..c6124e7177 100644 --- a/src/compiler/scala/tools/nsc/Global.scala +++ b/src/compiler/scala/tools/nsc/Global.scala @@ -1353,7 +1353,7 @@ class Global(var currentSettings: Settings, var reporter: Reporter) val inlineExceptionHandlersPhase = phaseNamed("inlinehandlers") val closelimPhase = phaseNamed("closelim") val dcePhase = phaseNamed("dce") - // val jvmPhase = phaseNamed("jvm") + val jvmPhase = phaseNamed("jvm") def runIsAt(ph: Phase) = globalPhase.id == ph.id def runIsAtOptimiz = { diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala index adaf870c46..58c1d0e497 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala @@ -643,7 +643,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { case Apply(fun @ _, List(expr)) if currentRun.runDefinitions.isBox(fun.symbol) => val nativeKind = tpeTK(expr) genLoad(expr, nativeKind) - val MethodNameAndType(mname, methodType) = asmBoxTo(nativeKind) + val MethodNameAndType(mname, methodType) = srBoxesRuntimeBoxToMethods(nativeKind) bc.invokestatic(srBoxesRunTimeRef.internalName, mname, methodType.descriptor, app.pos) generatedType = boxResultType(fun.symbol) // was typeToBType(fun.symbol.tpe.resultType) @@ -651,7 +651,7 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder { genLoad(expr) val boxType = unboxResultType(fun.symbol) // was typeToBType(fun.symbol.owner.linkedClassOfClass.tpe) generatedType = boxType - val MethodNameAndType(mname, methodType) = asmUnboxTo(boxType) + val MethodNameAndType(mname, methodType) = srBoxesRuntimeUnboxToMethods(boxType) bc.invokestatic(srBoxesRunTimeRef.internalName, mname, methodType.descriptor, app.pos) case app @ Apply(fun, args) => diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala index ef3fab7617..f1b9e8ea94 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala @@ -1113,7 +1113,7 @@ abstract class BTypes { */ /** - * Just a named pair, used in CoreBTypes.asmBoxTo/asmUnboxTo. + * Just a named pair, used in CoreBTypes.srBoxesRuntimeBoxToMethods/srBoxesRuntimeUnboxToMethods. */ final case class MethodNameAndType(name: String, methodType: MethodBType) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala index a2bf8e5725..35b84cc4a9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypesFromSymbols.scala @@ -109,7 +109,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { assert(classSym != NoSymbol, "Cannot create ClassBType from NoSymbol") assert(classSym.isClass, s"Cannot create ClassBType from non-class symbol $classSym") assertClassNotArrayNotPrimitive(classSym) - assert(!primitiveTypeMap.contains(classSym) || isCompilingPrimitive, s"Cannot create ClassBType for primitive class symbol $classSym") + assert(!primitiveTypeToBType.contains(classSym) || isCompilingPrimitive, s"Cannot create ClassBType for primitive class symbol $classSym") if (classSym == NothingClass) srNothingRef else if (classSym == NullClass) srNullRef else { @@ -155,7 +155,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { def primitiveOrClassToBType(sym: Symbol): BType = { assertClassNotArray(sym) assert(!sym.isImplClass, sym) - primitiveTypeMap.getOrElse(sym, classBTypeFromSymbol(sym)) + primitiveTypeToBType.getOrElse(sym, classBTypeFromSymbol(sym)) } /** @@ -217,7 +217,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { def assertClassNotArrayNotPrimitive(sym: Symbol): Unit = { assertClassNotArray(sym) - assert(!primitiveTypeMap.contains(sym) || isCompilingPrimitive, sym) + assert(!primitiveTypeToBType.contains(sym) || isCompilingPrimitive, sym) } private def setClassInfo(classSym: Symbol, classBType: ClassBType): ClassBType = { @@ -240,7 +240,7 @@ class BTypesFromSymbols[G <: Global](val global: G) extends BTypes { superClassSym == ObjectClass else // A ClassBType for a primitive class (scala.Boolean et al) is only created when compiling these classes. - ((superClassSym != NoSymbol) && !superClassSym.isInterface) || (isCompilingPrimitive && primitiveTypeMap.contains(classSym)), + ((superClassSym != NoSymbol) && !superClassSym.isInterface) || (isCompilingPrimitive && primitiveTypeToBType.contains(classSym)), s"Bad superClass for $classSym: $superClassSym" ) val superClass = if (superClassSym == NoSymbol) None diff --git a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala index 0317e08d9e..0eaf509133 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/CoreBTypes.scala @@ -1,7 +1,7 @@ package scala.tools.nsc package backend.jvm -import scala.annotation.switch +import scala.tools.nsc.backend.jvm.BTypes.InternalName /** * Core BTypes and some other definitions. The initialization of these definitions requires access @@ -29,14 +29,14 @@ import scala.annotation.switch class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { import bTypes._ import global._ - import rootMirror.{requiredClass, requiredModule, getClassIfDefined} + import rootMirror.{requiredClass, requiredModule, getRequiredClass, getClassIfDefined} import definitions._ /** * Maps primitive types to their corresponding PrimitiveBType. The map is defined lexically above * the first use of `classBTypeFromSymbol` because that method looks at the map. */ - lazy val primitiveTypeMap: Map[Symbol, PrimitiveBType] = Map( + lazy val primitiveTypeToBType: Map[Symbol, PrimitiveBType] = Map( UnitClass -> UNIT, BooleanClass -> BOOL, CharClass -> CHAR, @@ -45,34 +45,22 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { IntClass -> INT, LongClass -> LONG, FloatClass -> FLOAT, - DoubleClass -> DOUBLE - ) - - private lazy val BOXED_UNIT : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.Void]) - private lazy val BOXED_BOOLEAN : ClassBType = classBTypeFromSymbol(BoxedBooleanClass) - private lazy val BOXED_BYTE : ClassBType = classBTypeFromSymbol(BoxedByteClass) - private lazy val BOXED_SHORT : ClassBType = classBTypeFromSymbol(BoxedShortClass) - private lazy val BOXED_CHAR : ClassBType = classBTypeFromSymbol(BoxedCharacterClass) - private lazy val BOXED_INT : ClassBType = classBTypeFromSymbol(BoxedIntClass) - private lazy val BOXED_LONG : ClassBType = classBTypeFromSymbol(BoxedLongClass) - private lazy val BOXED_FLOAT : ClassBType = classBTypeFromSymbol(BoxedFloatClass) - private lazy val BOXED_DOUBLE : ClassBType = classBTypeFromSymbol(BoxedDoubleClass) + DoubleClass -> DOUBLE) /** * Map from primitive types to their boxed class type. Useful when pushing class literals onto the * operand stack (ldc instruction taking a class literal), see genConstant. */ lazy val boxedClassOfPrimitive: Map[PrimitiveBType, ClassBType] = Map( - UNIT -> BOXED_UNIT, - BOOL -> BOXED_BOOLEAN, - BYTE -> BOXED_BYTE, - SHORT -> BOXED_SHORT, - CHAR -> BOXED_CHAR, - INT -> BOXED_INT, - LONG -> BOXED_LONG, - FLOAT -> BOXED_FLOAT, - DOUBLE -> BOXED_DOUBLE - ) + UNIT -> classBTypeFromSymbol(requiredClass[java.lang.Void]), + BOOL -> classBTypeFromSymbol(BoxedBooleanClass), + BYTE -> classBTypeFromSymbol(BoxedByteClass), + SHORT -> classBTypeFromSymbol(BoxedShortClass), + CHAR -> classBTypeFromSymbol(BoxedCharacterClass), + INT -> classBTypeFromSymbol(BoxedIntClass), + LONG -> classBTypeFromSymbol(BoxedLongClass), + FLOAT -> classBTypeFromSymbol(BoxedFloatClass), + DOUBLE -> classBTypeFromSymbol(BoxedDoubleClass)) lazy val boxedClasses: Set[ClassBType] = boxedClassOfPrimitive.values.toSet @@ -82,7 +70,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { */ lazy val boxResultType: Map[Symbol, ClassBType] = { for ((valueClassSym, boxMethodSym) <- currentRun.runDefinitions.boxMethod) - yield boxMethodSym -> boxedClassOfPrimitive(primitiveTypeMap(valueClassSym)) + yield boxMethodSym -> boxedClassOfPrimitive(primitiveTypeToBType(valueClassSym)) } /** @@ -90,7 +78,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { * For example, the method symbol for `Byte.unbox()`) is mapped to the PrimitiveBType BYTE. */ lazy val unboxResultType: Map[Symbol, PrimitiveBType] = { for ((valueClassSym, unboxMethodSym) <- currentRun.runDefinitions.unboxMethod) - yield unboxMethodSym -> primitiveTypeMap(valueClassSym) + yield unboxMethodSym -> primitiveTypeToBType(valueClassSym) } /* @@ -106,6 +94,7 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { lazy val ObjectRef : ClassBType = classBTypeFromSymbol(ObjectClass) lazy val StringRef : ClassBType = classBTypeFromSymbol(StringClass) + lazy val PredefRef : ClassBType = classBTypeFromSymbol(PredefModule.moduleClass) lazy val jlStringBuilderRef : ClassBType = classBTypeFromSymbol(StringBuilderClass) lazy val jlThrowableRef : ClassBType = classBTypeFromSymbol(ThrowableClass) lazy val jlCloneableRef : ClassBType = classBTypeFromSymbol(JavaCloneableClass) // java/lang/Cloneable @@ -116,56 +105,135 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { lazy val sbScalaBeanInfoRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.beans.ScalaBeanInfo]) lazy val jliSerializedLambdaRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.SerializedLambda]) lazy val jliMethodHandlesRef : ClassBType = classBTypeFromSymbol(requiredClass[java.lang.invoke.MethodHandles]) - lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(exitingPickler(rootMirror.getRequiredClass("java.lang.invoke.MethodHandles.Lookup"))) // didn't find a reliable non-stringly-typed way that works for inner classes in the backend + lazy val jliMethodHandlesLookupRef : ClassBType = classBTypeFromSymbol(exitingPickler(getRequiredClass("java.lang.invoke.MethodHandles.Lookup"))) // didn't find a reliable non-stringly-typed way that works for inner classes in the backend lazy val srLambdaDeserializerRef : ClassBType = classBTypeFromSymbol(requiredModule[scala.runtime.LambdaDeserializer.type].moduleClass) lazy val srBoxesRunTimeRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxesRunTime]) lazy val srBoxedUnitRef : ClassBType = classBTypeFromSymbol(requiredClass[scala.runtime.BoxedUnit]) - lazy val hashMethodSym: Symbol = getMember(ScalaRunTimeModule, nme.hash_) + private def methodNameAndType(cls: Symbol, name: Name, static: Boolean = false, filterOverload: Symbol => Boolean = _ => true): MethodNameAndType = { + val holder = if (static) cls.companionModule.moduleClass else cls + val method = holder.info.member(name).suchThat(filterOverload) + assert(!method.isOverloaded, method) + MethodNameAndType(name.toString, methodBTypeFromSymbol(method)) + } - // TODO @lry avoiding going through through missingHook for every line in the REPL: https://github.com/scala/scala/commit/8d962ed4ddd310cc784121c426a2e3f56a112540 - lazy val AndroidParcelableInterface : Symbol = getClassIfDefined("android.os.Parcelable") - lazy val AndroidCreatorClass : Symbol = getClassIfDefined("android.os.Parcelable$Creator") + private def srBoxesRuntimeMethods(getName: (String, String) => String): Map[BType, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val bType = primitiveTypeToBType(primitive) + val name = newTermName(getName(primitive.name.toString, boxedClass(primitive).name.toString)) + (bType, methodNameAndType(BoxesRunTimeClass, name)) + })(collection.breakOut) + } - lazy val BeanInfoAttr: Symbol = requiredClass[scala.beans.BeanInfo] + // Z -> MethodNameAndType(boxToBoolean,(Z)Ljava/lang/Boolean;) + lazy val srBoxesRuntimeBoxToMethods: Map[BType, MethodNameAndType] = srBoxesRuntimeMethods((primitive, boxed) => "boxTo" + boxed) - /* The Object => String overload. */ - lazy val String_valueOf: Symbol = { - getMember(StringModule, nme.valueOf) filter (sym => sym.info.paramTypes match { - case List(pt) => pt.typeSymbol == ObjectClass - case _ => false - }) + // Z -> MethodNameAndType(unboxToBoolean,(Ljava/lang/Object;)Z) + lazy val srBoxesRuntimeUnboxToMethods: Map[BType, MethodNameAndType] = srBoxesRuntimeMethods((primitive, boxed) => "unboxTo" + primitive) + + def singleParamOfClass(cls: Symbol) = (s: Symbol) => s.paramss match { + case List(List(param)) => param.info.typeSymbol == cls + case _ => false } - /** - * Methods in scala.runtime.BoxesRuntime - */ - lazy val asmBoxTo : Map[BType, MethodNameAndType] = Map( - BOOL -> MethodNameAndType("boxToBoolean", MethodBType(List(BOOL), BOXED_BOOLEAN)), - BYTE -> MethodNameAndType("boxToByte", MethodBType(List(BYTE), BOXED_BYTE)), - CHAR -> MethodNameAndType("boxToCharacter", MethodBType(List(CHAR), BOXED_CHAR)), - SHORT -> MethodNameAndType("boxToShort", MethodBType(List(SHORT), BOXED_SHORT)), - INT -> MethodNameAndType("boxToInteger", MethodBType(List(INT), BOXED_INT)), - LONG -> MethodNameAndType("boxToLong", MethodBType(List(LONG), BOXED_LONG)), - FLOAT -> MethodNameAndType("boxToFloat", MethodBType(List(FLOAT), BOXED_FLOAT)), - DOUBLE -> MethodNameAndType("boxToDouble", MethodBType(List(DOUBLE), BOXED_DOUBLE)) - ) - - lazy val asmUnboxTo: Map[BType, MethodNameAndType] = Map( - BOOL -> MethodNameAndType("unboxToBoolean", MethodBType(List(ObjectRef), BOOL)), - BYTE -> MethodNameAndType("unboxToByte", MethodBType(List(ObjectRef), BYTE)), - CHAR -> MethodNameAndType("unboxToChar", MethodBType(List(ObjectRef), CHAR)), - SHORT -> MethodNameAndType("unboxToShort", MethodBType(List(ObjectRef), SHORT)), - INT -> MethodNameAndType("unboxToInt", MethodBType(List(ObjectRef), INT)), - LONG -> MethodNameAndType("unboxToLong", MethodBType(List(ObjectRef), LONG)), - FLOAT -> MethodNameAndType("unboxToFloat", MethodBType(List(ObjectRef), FLOAT)), - DOUBLE -> MethodNameAndType("unboxToDouble", MethodBType(List(ObjectRef), DOUBLE)) - ) + // java/lang/Boolean -> MethodNameAndType(valueOf,(Z)Ljava/lang/Boolean;) + lazy val javaBoxMethods: Map[InternalName, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + val method = methodNameAndType(boxed, newTermName("valueOf"), static = true, filterOverload = singleParamOfClass(primitive)) + (classBTypeFromSymbol(boxed).internalName, method) + })(collection.breakOut) + } + + // java/lang/Boolean -> MethodNameAndType(booleanValue,()Z) + lazy val javaUnboxMethods: Map[InternalName, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + val name = primitive.name.toString.toLowerCase + "Value" + (classBTypeFromSymbol(boxed).internalName, methodNameAndType(boxed, newTermName(name))) + })(collection.breakOut) + } + + private def predefBoxingMethods(getName: (String, String) => String): Map[String, MethodBType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + val name = getName(primitive.name.toString, boxed.name.toString) + (name, methodNameAndType(PredefModule.moduleClass, newTermName(name)).methodType) + })(collection.breakOut) + } + + // boolean2Boolean -> (Z)Ljava/lang/Boolean; + lazy val predefAutoBoxMethods: Map[String, MethodBType] = predefBoxingMethods((primitive, boxed) => primitive.toLowerCase + "2" + boxed) + + // Boolean2boolean -> (Ljava/lang/Boolean;)Z + lazy val predefAutoUnboxMethods: Map[String, MethodBType] = predefBoxingMethods((primitive, boxed) => boxed + "2" + primitive.toLowerCase) + + private def staticRefMethods(name: Name): Map[InternalName, MethodNameAndType] = { + allRefClasses.map(refClass => + (classBTypeFromSymbol(refClass).internalName, methodNameAndType(refClass, name, static = true)))(collection.breakOut) + } + + // scala/runtime/BooleanRef -> MethodNameAndType(create,(Z)Lscala/runtime/BooleanRef;) + lazy val srRefCreateMethods: Map[InternalName, MethodNameAndType] = staticRefMethods(nme.create) + + // scala/runtime/BooleanRef -> MethodNameAndType(zero,()Lscala/runtime/BooleanRef;) + lazy val srRefZeroMethods: Map[InternalName, MethodNameAndType] = staticRefMethods(nme.zero) + + // java/lang/Boolean -> MethodNameAndType(,(Z)V) + lazy val primitiveBoxConstructors: Map[InternalName, MethodNameAndType] = { + ScalaValueClassesNoUnit.map(primitive => { + val boxed = boxedClass(primitive) + (classBTypeFromSymbol(boxed).internalName, methodNameAndType(boxed, nme.CONSTRUCTOR, filterOverload = singleParamOfClass(primitive))) + })(collection.breakOut) + } + + private def nonOverloadedConstructors(classes: Iterable[Symbol]): Map[InternalName, MethodNameAndType] = { + classes.map(cls => (classBTypeFromSymbol(cls).internalName, methodNameAndType(cls, nme.CONSTRUCTOR)))(collection.breakOut) + } + + // scala/runtime/BooleanRef -> MethodNameAndType(,(Z)V) + lazy val srRefConstructors: Map[InternalName, MethodNameAndType] = nonOverloadedConstructors(allRefClasses) + + private def specializedSubclasses(cls: Symbol): List[Symbol] = { + exitingSpecialize(cls.info) // the `transformInfo` method of specialization adds specialized subclasses to the `specializedClass` map + specializeTypes.specializedClass.collect({ + case ((`cls`, _), specCls) => specCls + }).toList + } + + // scala/Tuple3 -> MethodNameAndType(,(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V) + // scala/Tuple2$mcZC$sp -> MethodNameAndType(,(ZC)V) + lazy val tupleClassConstructors: Map[InternalName, MethodNameAndType] = { + val tupleClassSymbols = TupleClass.seq ++ specializedSubclasses(TupleClass(1)) ++ specializedSubclasses(TupleClass(2)) + nonOverloadedConstructors(tupleClassSymbols) + } + + // enumeration of specialized classes is temporary, while we still use the java-defined JFunctionN. + // once we switch to ordinary FunctionN, we can use specializedSubclasses just like for tuples. + private def functionClasses(base: String): Set[Symbol] = { + def primitives = Iterator("B", "S", "I", "J", "C", "F", "D", "Z", "V") + def ijfd = Iterator("I", "J", "F", "D") + def ijfdzv = Iterator("I", "J", "F", "D", "Z", "V") + def ijd = Iterator("I", "J", "D") + val classNames = Set.empty[String] ++ { + (0 to 22).map(base + _) + } ++ { + primitives.map(base + "0$mc" + _ + "$sp") // Function0 + } ++ { + // return type specializations appear first in the name string (alphabetical sorting) + for (r <- ijfdzv; a <- ijfd) yield base + "1$mc" + r + a + "$sp" // Function1 + } ++ { + for (r <- ijfdzv; a <- ijd; b <- ijd) yield base + "2$mc" + r + a + b + "$sp" // Function2 + } + classNames map getRequiredClass + } + + lazy val srJFunctionRefs: Set[InternalName] = functionClasses("scala.runtime.java8.JFunction").map(classBTypeFromSymbol(_).internalName) lazy val typeOfArrayOp: Map[Int, BType] = { import scalaPrimitives._ Map( - (List(ZARRAY_LENGTH, ZARRAY_GET, ZARRAY_SET) map (_ -> BOOL)) ++ + (List(ZARRAY_LENGTH, ZARRAY_GET, ZARRAY_SET) map (_ -> BOOL)) ++ (List(BARRAY_LENGTH, BARRAY_GET, BARRAY_SET) map (_ -> BYTE)) ++ (List(SARRAY_LENGTH, SARRAY_GET, SARRAY_SET) map (_ -> SHORT)) ++ (List(CARRAY_LENGTH, CARRAY_GET, CARRAY_SET) map (_ -> CHAR)) ++ @@ -176,6 +244,22 @@ class CoreBTypes[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: BTFS) { (List(OARRAY_LENGTH, OARRAY_GET, OARRAY_SET) map (_ -> ObjectRef)) : _* ) } + + lazy val hashMethodSym: Symbol = getMember(ScalaRunTimeModule, nme.hash_) + + // TODO @lry avoiding going through through missingHook for every line in the REPL: https://github.com/scala/scala/commit/8d962ed4ddd310cc784121c426a2e3f56a112540 + lazy val AndroidParcelableInterface : Symbol = getClassIfDefined("android.os.Parcelable") + lazy val AndroidCreatorClass : Symbol = getClassIfDefined("android.os.Parcelable$Creator") + + lazy val BeanInfoAttr: Symbol = requiredClass[scala.beans.BeanInfo] + + /* The Object => String overload. */ + lazy val String_valueOf: Symbol = { + getMember(StringModule, nme.valueOf) filter (sym => sym.info.paramTypes match { + case List(pt) => pt.typeSymbol == ObjectClass + case _ => false + }) + } } /** @@ -191,10 +275,14 @@ trait CoreBTypesProxyGlobalIndependent[BTS <: BTypes] { import bTypes._ def boxedClasses: Set[ClassBType] + def boxedClassOfPrimitive: Map[PrimitiveBType, ClassBType] - def ObjectRef : ClassBType def srNothingRef : ClassBType def srNullRef : ClassBType + + def ObjectRef : ClassBType + def StringRef : ClassBType + def PredefRef : ClassBType def jlCloneableRef : ClassBType def jiSerializableRef : ClassBType def juHashMapRef : ClassBType @@ -206,8 +294,23 @@ trait CoreBTypesProxyGlobalIndependent[BTS <: BTypes] { def srBoxesRunTimeRef : ClassBType def srBoxedUnitRef : ClassBType - def asmBoxTo : Map[BType, MethodNameAndType] - def asmUnboxTo: Map[BType, MethodNameAndType] + def srBoxesRuntimeBoxToMethods : Map[BType, MethodNameAndType] + def srBoxesRuntimeUnboxToMethods : Map[BType, MethodNameAndType] + + def javaBoxMethods : Map[InternalName, MethodNameAndType] + def javaUnboxMethods : Map[InternalName, MethodNameAndType] + + def predefAutoBoxMethods : Map[String, MethodBType] + def predefAutoUnboxMethods : Map[String, MethodBType] + + def srRefCreateMethods : Map[InternalName, MethodNameAndType] + def srRefZeroMethods : Map[InternalName, MethodNameAndType] + + def primitiveBoxConstructors : Map[InternalName, MethodNameAndType] + def srRefConstructors : Map[InternalName, MethodNameAndType] + def tupleClassConstructors : Map[InternalName, MethodNameAndType] + + def srJFunctionRefs: Set[InternalName] } /** @@ -222,20 +325,21 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: _coreBTypes = coreBTypes.asInstanceOf[CoreBTypes[bTypes.type]] } - def primitiveTypeMap: Map[Symbol, PrimitiveBType] = _coreBTypes.primitiveTypeMap + def primitiveTypeToBType: Map[Symbol, PrimitiveBType] = _coreBTypes.primitiveTypeToBType def boxedClasses: Set[ClassBType] = _coreBTypes.boxedClasses - def boxedClassOfPrimitive: Map[PrimitiveBType, ClassBType] = _coreBTypes.boxedClassOfPrimitive def boxResultType: Map[Symbol, ClassBType] = _coreBTypes.boxResultType def unboxResultType: Map[Symbol, PrimitiveBType] = _coreBTypes.unboxResultType + def srNothingRef : ClassBType = _coreBTypes.srNothingRef + def srNullRef : ClassBType = _coreBTypes.srNullRef + def ObjectRef : ClassBType = _coreBTypes.ObjectRef def StringRef : ClassBType = _coreBTypes.StringRef + def PredefRef : ClassBType = _coreBTypes.PredefRef def jlStringBuilderRef : ClassBType = _coreBTypes.jlStringBuilderRef - def srNothingRef : ClassBType = _coreBTypes.srNothingRef - def srNullRef : ClassBType = _coreBTypes.srNullRef def jlThrowableRef : ClassBType = _coreBTypes.jlThrowableRef def jlCloneableRef : ClassBType = _coreBTypes.jlCloneableRef def jiSerializableRef : ClassBType = _coreBTypes.jiSerializableRef @@ -250,6 +354,28 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: def srBoxesRunTimeRef : ClassBType = _coreBTypes.srBoxesRunTimeRef def srBoxedUnitRef : ClassBType = _coreBTypes.srBoxedUnitRef + def srBoxesRuntimeBoxToMethods : Map[BType, MethodNameAndType] = _coreBTypes.srBoxesRuntimeBoxToMethods + def srBoxesRuntimeUnboxToMethods : Map[BType, MethodNameAndType] = _coreBTypes.srBoxesRuntimeUnboxToMethods + + def javaBoxMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.javaBoxMethods + def javaUnboxMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.javaUnboxMethods + + def predefAutoBoxMethods : Map[String, MethodBType] = _coreBTypes.predefAutoBoxMethods + def predefAutoUnboxMethods : Map[String, MethodBType] = _coreBTypes.predefAutoUnboxMethods + + def srRefCreateMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.srRefCreateMethods + def srRefZeroMethods : Map[InternalName, MethodNameAndType] = _coreBTypes.srRefZeroMethods + + def primitiveBoxConstructors : Map[InternalName, MethodNameAndType] = _coreBTypes.primitiveBoxConstructors + def srRefConstructors : Map[InternalName, MethodNameAndType] = _coreBTypes.srRefConstructors + def tupleClassConstructors : Map[InternalName, MethodNameAndType] = _coreBTypes.tupleClassConstructors + + def srJFunctionRefs: Set[InternalName] = _coreBTypes.srJFunctionRefs + + def typeOfArrayOp: Map[Int, BType] = _coreBTypes.typeOfArrayOp + + // Some symbols. These references should probably be moved to Definitions. + def hashMethodSym: Symbol = _coreBTypes.hashMethodSym def AndroidParcelableInterface : Symbol = _coreBTypes.AndroidParcelableInterface @@ -258,9 +384,4 @@ final class CoreBTypesProxy[BTFS <: BTypesFromSymbols[_ <: Global]](val bTypes: def BeanInfoAttr: Symbol = _coreBTypes.BeanInfoAttr def String_valueOf: Symbol = _coreBTypes.String_valueOf - - def asmBoxTo : Map[BType, MethodNameAndType] = _coreBTypes.asmBoxTo - def asmUnboxTo: Map[BType, MethodNameAndType] = _coreBTypes.asmUnboxTo - - def typeOfArrayOp: Map[Int, BType] = _coreBTypes.typeOfArrayOp } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala index baf82032f7..e8630c65d9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/BackendUtils.scala @@ -3,9 +3,11 @@ package backend.jvm package analysis import scala.annotation.switch -import scala.tools.asm.{Opcodes, Handle, Type, Label} +import scala.tools.asm.{Handle, Type, Label} +import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis.{Frame, BasicInterpreter, Analyzer, Value} +import GenBCode._ import scala.tools.nsc.backend.jvm.BTypes._ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ @@ -24,6 +26,7 @@ import scala.collection.convert.decorateAsScala._ */ class BackendUtils[BT <: BTypes](val btypes: BT) { import btypes._ + import btypes.coreBTypes._ import callGraph.ClosureInstantiation /** @@ -73,8 +76,6 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { */ def addLambdaDeserialize(classNode: ClassNode): Unit = { val cw = classNode - import scala.tools.asm.Opcodes._ - import btypes.coreBTypes._ // Make sure to reference the ClassBTypes of all types that are used in the code generated // here (e.g. java/util/Map) are initialized. Initializing a ClassBType adds it to the @@ -145,14 +146,137 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { (result, map, hasSerializableClosureInstantiation) } - def getBoxedUnit: FieldInsnNode = new FieldInsnNode(Opcodes.GETSTATIC, coreBTypes.srBoxedUnitRef.internalName, "UNIT", coreBTypes.srBoxedUnitRef.descriptor) + def getBoxedUnit: FieldInsnNode = new FieldInsnNode(GETSTATIC, srBoxedUnitRef.internalName, "UNIT", srBoxedUnitRef.descriptor) private val anonfunAdaptedName = """.*\$anonfun\$\d+\$adapted""".r def hasAdaptedImplMethod(closureInit: ClosureInstantiation): Boolean = { - BytecodeUtils.isrJFunctionType(Type.getReturnType(closureInit.lambdaMetaFactoryCall.indy.desc).getInternalName) && + isrJFunctionType(Type.getReturnType(closureInit.lambdaMetaFactoryCall.indy.desc).getInternalName) && anonfunAdaptedName.pattern.matcher(closureInit.lambdaMetaFactoryCall.implMethod.getName).matches } + private def primitiveAsmTypeToBType(primitiveType: Type): PrimitiveBType = (primitiveType.getSort: @switch) match { + case Type.BOOLEAN => BOOL + case Type.BYTE => BYTE + case Type.CHAR => CHAR + case Type.SHORT => SHORT + case Type.INT => INT + case Type.LONG => LONG + case Type.FLOAT => FLOAT + case Type.DOUBLE => DOUBLE + case _ => null + } + + def isScalaBox(insn: MethodInsnNode): Boolean = { + insn.owner == srBoxesRunTimeRef.internalName && { + val args = Type.getArgumentTypes(insn.desc) + args.length == 1 && (srBoxesRuntimeBoxToMethods.get(primitiveAsmTypeToBType(args(0))) match { + case Some(MethodNameAndType(name, tp)) => name == insn.name && tp.descriptor == insn.desc + case _ => false + }) + } + } + + def getScalaBox(primitiveType: Type): MethodInsnNode = { + val bType = primitiveAsmTypeToBType(primitiveType) + val MethodNameAndType(name, methodBType) = srBoxesRuntimeBoxToMethods(bType) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + def isScalaUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == srBoxesRunTimeRef.internalName && (srBoxesRuntimeUnboxToMethods.get(primitiveAsmTypeToBType(Type.getReturnType(insn.desc))) match { + case Some(MethodNameAndType(name, tp)) => name == insn.name && tp.descriptor == insn.desc + case _ => false + }) + } + + def getScalaUnbox(primitiveType: Type): MethodInsnNode = { + val bType = primitiveAsmTypeToBType(primitiveType) + val MethodNameAndType(name, methodBType) = srBoxesRuntimeUnboxToMethods(bType) + new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) + } + + private def calleeInMap(insn: MethodInsnNode, map: Map[InternalName, MethodNameAndType]): Boolean = map.get(insn.owner) match { + case Some(MethodNameAndType(name, tp)) => insn.name == name && insn.desc == tp.descriptor + case _ => false + } + + def isJavaBox(insn: MethodInsnNode): Boolean = calleeInMap(insn, javaBoxMethods) + def isJavaUnbox(insn: MethodInsnNode): Boolean = calleeInMap(insn, javaUnboxMethods) + + def isPredefAutoBox(insn: MethodInsnNode): Boolean = { + insn.owner == PredefRef.internalName && (predefAutoBoxMethods.get(insn.name) match { + case Some(tp) => insn.desc == tp.descriptor + case _ => false + }) + } + + def isPredefAutoUnbox(insn: MethodInsnNode): Boolean = { + insn.owner == PredefRef.internalName && (predefAutoUnboxMethods.get(insn.name) match { + case Some(tp) => insn.desc == tp.descriptor + case _ => false + }) + } + + def isRefCreate(insn: MethodInsnNode): Boolean = calleeInMap(insn, srRefCreateMethods) + def isRefZero(insn: MethodInsnNode): Boolean = calleeInMap(insn, srRefZeroMethods) + + def runtimeRefClassBoxedType(refClass: InternalName): Type = Type.getArgumentTypes(srRefCreateMethods(refClass).methodType.descriptor)(0) + + def isSideEffectFreeCall(insn: MethodInsnNode): Boolean = { + isScalaBox(insn) || isScalaUnbox(insn) || + isJavaBox(insn) || // not java unbox, it may NPE + isSideEffectFreeConstructorCall(insn) + } + + def isNonNullMethodInvocation(mi: MethodInsnNode): Boolean = { + isJavaBox(mi) || isScalaBox(mi) || isPredefAutoBox(mi) || isRefCreate(mi) || isRefZero(mi) + } + + def isModuleLoad(insn: AbstractInsnNode, moduleName: InternalName): Boolean = insn match { + case fi: FieldInsnNode => fi.getOpcode == GETSTATIC && fi.owner == moduleName && fi.name == "MODULE$" && fi.desc == ("L" + moduleName + ";") + case _ => false + } + + def isPredefLoad(insn: AbstractInsnNode) = isModuleLoad(insn, PredefRef.internalName) + + def isPrimitiveBoxConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, primitiveBoxConstructors) + def isRuntimeRefConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, srRefConstructors) + def isTupleConstructor(insn: MethodInsnNode): Boolean = calleeInMap(insn, tupleClassConstructors) + + // unused objects created by these constructors are eliminated by pushPop + private lazy val sideEffectFreeConstructors: Set[(String, String)] = { + val ownerDesc = (p: (InternalName, MethodNameAndType)) => (p._1, p._2.methodType.descriptor) + primitiveBoxConstructors.map(ownerDesc).toSet ++ + srRefConstructors.map(ownerDesc) ++ + tupleClassConstructors.map(ownerDesc) ++ Set( + (ObjectRef.internalName, MethodBType(Nil, UNIT).descriptor), + (StringRef.internalName, MethodBType(Nil, UNIT).descriptor), + (StringRef.internalName, MethodBType(List(StringRef), UNIT).descriptor), + (StringRef.internalName, MethodBType(List(ArrayBType(CHAR)), UNIT).descriptor)) + } + + def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { + insn.name == INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors((insn.owner, insn.desc)) + } + + private lazy val classesOfSideEffectFreeConstructors = sideEffectFreeConstructors.map(_._1) + + def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode) = { + insn.getOpcode == NEW && { + val ti = insn.asInstanceOf[TypeInsnNode] + classesOfSideEffectFreeConstructors.contains(ti.desc) + } + } + + def isBoxedUnit(insn: AbstractInsnNode) = { + insn.getOpcode == GETSTATIC && { + val fi = insn.asInstanceOf[FieldInsnNode] + fi.owner == srBoxedUnitRef.internalName && fi.name == "UNIT" && fi.desc == srBoxedUnitRef.descriptor + } + } + + def isrJFunctionType(internalName: InternalName): Boolean = srJFunctionRefs(internalName) + /** * Visit the class node and collect all referenced nested classes. */ @@ -287,15 +411,13 @@ class BackendUtils[BT <: BTypes](val btypes: BT) { * Analyzer: its implementation also skips over unreachable code in the same way. */ def computeMaxLocalsMaxStack(method: MethodNode): Unit = { - import Opcodes._ - if (isAbstractMethod(method) || isNativeMethod(method)) { method.maxLocals = 0 method.maxStack = 0 } else if (!maxLocalsMaxStackComputed(method)) { val size = method.instructions.size - var maxLocals = (Type.getArgumentsAndReturnSizes(method.desc) >> 2) - (if (isStaticMethod(method)) 1 else 0) + var maxLocals = parametersSize(method) var maxStack = 0 // queue of instruction indices where analysis should start diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala index 2181f00850..dd19ad594f 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/InstructionStackEffect.scala @@ -335,5 +335,4 @@ object InstructionStackEffect { IFNONNULL => t(1, 0) } } - } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala b/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala index 1078d0085e..30e73f8ac2 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzer.scala @@ -63,7 +63,7 @@ object NullnessValue { def unknown(insn: AbstractInsnNode) = if (BytecodeUtils.instructionResultSize(insn) == 2) UnknownValue2 else UnknownValue1 } -final class NullnessInterpreter extends Interpreter[NullnessValue](Opcodes.ASM5) { +final class NullnessInterpreter(bTypes: BTypes) extends Interpreter[NullnessValue](Opcodes.ASM5) { def newValue(tp: Type): NullnessValue = { // ASM loves giving semantics to null. The behavior here is the same as in SourceInterpreter, // which is provided by the framework. @@ -114,7 +114,7 @@ final class NullnessInterpreter extends Interpreter[NullnessValue](Opcodes.ASM5) def ternaryOperation(insn: AbstractInsnNode, value1: NullnessValue, value2: NullnessValue, value3: NullnessValue): NullnessValue = UnknownValue1 def naryOperation(insn: AbstractInsnNode, values: util.List[_ <: NullnessValue]): NullnessValue = insn match { - case mi: MethodInsnNode if isNonNullMethodInvocation(mi) => + case mi: MethodInsnNode if bTypes.backendUtils.isNonNullMethodInvocation(mi) => NotNullValue case _ => @@ -197,7 +197,7 @@ class NullnessFrame(nLocals: Int, nStack: Int) extends AliasingFrame[NullnessVal * This class is required to override the `newFrame` methods, which makes makes sure the analyzer * uses NullnessFrames. */ -class NullnessAnalyzer extends Analyzer[NullnessValue](new NullnessInterpreter) { +class NullnessAnalyzer(bTypes: BTypes) extends Analyzer[NullnessValue](new NullnessInterpreter(bTypes)) { override def newFrame(nLocals: Int, nStack: Int): NullnessFrame = new NullnessFrame(nLocals, nStack) override def newFrame(src: Frame[_ <: NullnessValue]): NullnessFrame = new NullnessFrame(src) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala index f83167eabf..ff36f36589 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala @@ -348,291 +348,6 @@ object BytecodeUtils { } } - def isSideEffectFreeCall(insn: MethodInsnNode): Boolean = { - isScalaBox(insn) || isScalaUnbox(insn) || - isJavaBox(insn) || // not java unbox, it may NPE - isSideEffectFreeConstructorCall(insn) - } - - def isNonNullMethodInvocation(mi: MethodInsnNode): Boolean = { - isJavaBox(mi) || isScalaBox(mi) || isPredefAutoBox(mi) || isRefCreate(mi) || isRefZero(mi) - } - - private val srBoxesRunTimeName = "scala/runtime/BoxesRunTime" - - private val boxToMethods = Map( - Type.BOOLEAN -> ("boxToBoolean", "(Z)Ljava/lang/Boolean;"), - Type.BYTE -> ("boxToByte", "(B)Ljava/lang/Byte;"), - Type.CHAR -> ("boxToCharacter", "(C)Ljava/lang/Character;"), - Type.SHORT -> ("boxToShort", "(S)Ljava/lang/Short;"), - Type.INT -> ("boxToInteger", "(I)Ljava/lang/Integer;"), - Type.LONG -> ("boxToLong", "(J)Ljava/lang/Long;"), - Type.FLOAT -> ("boxToFloat", "(F)Ljava/lang/Float;"), - Type.DOUBLE -> ("boxToDouble", "(D)Ljava/lang/Double;")) - - def isScalaBox(insn: MethodInsnNode): Boolean = { - insn.owner == srBoxesRunTimeName && { - val args = Type.getArgumentTypes(insn.desc) - args.length == 1 && (boxToMethods.get(args(0).getSort) match { - case Some((name, desc)) => name == insn.name && desc == insn.desc - case _ => false - }) - } - } - - def getScalaBox(primitiveType: Type): MethodInsnNode = { - val (method, desc) = boxToMethods(primitiveType.getSort) - new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeName, method, desc, /*itf =*/ false) - } - - private val unboxToMethods = Map( - Type.BOOLEAN -> ("unboxToBoolean", "(Ljava/lang/Object;)Z"), - Type.BYTE -> ("unboxToByte", "(Ljava/lang/Object;)B"), - Type.CHAR -> ("unboxToChar", "(Ljava/lang/Object;)C"), - Type.SHORT -> ("unboxToShort", "(Ljava/lang/Object;)S"), - Type.INT -> ("unboxToInt", "(Ljava/lang/Object;)I"), - Type.LONG -> ("unboxToLong", "(Ljava/lang/Object;)J"), - Type.FLOAT -> ("unboxToFloat", "(Ljava/lang/Object;)F"), - Type.DOUBLE -> ("unboxToDouble", "(Ljava/lang/Object;)D")) - - def isScalaUnbox(insn: MethodInsnNode): Boolean = { - insn.owner == srBoxesRunTimeName && (unboxToMethods.get(Type.getReturnType(insn.desc).getSort) match { - case Some((name, desc)) => name == insn.name && desc == insn.desc - case _ => false - }) - } - - def getScalaUnbox(primitiveType: Type): MethodInsnNode = { - val (method, desc) = unboxToMethods(primitiveType.getSort) - new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeName, method, desc, /*itf =*/ false) - } - - def isJavaBox(insn: MethodInsnNode): Boolean = { - insn.name == "valueOf" && { - val args = Type.getArgumentTypes(insn.desc) - args.length == 1 && ((args(0).getSort: @switch) match { - case Type.BOOLEAN => insn.owner == "java/lang/Boolean" && insn.desc == "(Z)Ljava/lang/Boolean;" - case Type.BYTE => insn.owner == "java/lang/Byte" && insn.desc == "(B)Ljava/lang/Byte;" - case Type.CHAR => insn.owner == "java/lang/Character" && insn.desc == "(C)Ljava/lang/Character;" - case Type.SHORT => insn.owner == "java/lang/Short" && insn.desc == "(S)Ljava/lang/Short;" - case Type.INT => insn.owner == "java/lang/Integer" && insn.desc == "(I)Ljava/lang/Integer;" - case Type.LONG => insn.owner == "java/lang/Long" && insn.desc == "(J)Ljava/lang/Long;" - case Type.FLOAT => insn.owner == "java/lang/Float" && insn.desc == "(F)Ljava/lang/Float;" - case Type.DOUBLE => insn.owner == "java/lang/Double" && insn.desc == "(D)Ljava/lang/Double;" - case _ => false - }) - } - } - - def isJavaUnbox(insn: MethodInsnNode): Boolean = { - insn.desc.startsWith("()") && { - (Type.getReturnType(insn.desc).getSort: @switch) match { - case Type.BOOLEAN => insn.owner == "java/lang/Boolean" && insn.name == "booleanValue" - case Type.BYTE => insn.owner == "java/lang/Byte" && insn.name == "byteValue" - case Type.CHAR => insn.owner == "java/lang/Character" && insn.name == "charValue" - case Type.SHORT => insn.owner == "java/lang/Short" && insn.name == "shortValue" - case Type.INT => insn.owner == "java/lang/Integer" && insn.name == "intValue" - case Type.LONG => insn.owner == "java/lang/Long" && insn.name == "longValue" - case Type.FLOAT => insn.owner == "java/lang/Float" && insn.name == "floatValue" - case Type.DOUBLE => insn.owner == "java/lang/Double" && insn.name == "doubleValue" - case _ => false - } - } - } - - def isPredefAutoBox(insn: MethodInsnNode): Boolean = { - insn.owner == "scala/Predef$" && { - val args = Type.getArgumentTypes(insn.desc) - args.length == 1 && ((args(0).getSort: @switch) match { - case Type.BOOLEAN => insn.name == "boolean2Boolean" && insn.desc == "(Z)Ljava/lang/Boolean;" - case Type.BYTE => insn.name == "byte2Byte" && insn.desc == "(B)Ljava/lang/Byte;" - case Type.CHAR => insn.name == "char2Character" && insn.desc == "(C)Ljava/lang/Character;" - case Type.SHORT => insn.name == "short2Short" && insn.desc == "(S)Ljava/lang/Short;" - case Type.INT => insn.name == "int2Integer" && insn.desc == "(I)Ljava/lang/Integer;" - case Type.LONG => insn.name == "long2Long" && insn.desc == "(J)Ljava/lang/Long;" - case Type.FLOAT => insn.name == "float2Float" && insn.desc == "(F)Ljava/lang/Float;" - case Type.DOUBLE => insn.name == "double2Double" && insn.desc == "(D)Ljava/lang/Double;" - case _ => false - }) - } - } - - def isPredefAutoUnbox(insn: MethodInsnNode): Boolean = { - insn.owner == "scala/Predef$" && { - (Type.getReturnType(insn.desc).getSort: @switch) match { - case Type.BOOLEAN => insn.name == "Boolean2boolean" && insn.desc == "(Ljava/lang/Boolean;)Z" - case Type.BYTE => insn.name == "Byte2byte" && insn.desc == "(Ljava/lang/Byte;)B" - case Type.CHAR => insn.name == "Character2char" && insn.desc == "(Ljava/lang/Character;)C" - case Type.SHORT => insn.name == "Short2short" && insn.desc == "(Ljava/lang/Short;)S" - case Type.INT => insn.name == "Integer2int" && insn.desc == "(Ljava/lang/Integer;)I" - case Type.LONG => insn.name == "Long2long" && insn.desc == "(Ljava/lang/Long;)J" - case Type.FLOAT => insn.name == "Float2float" && insn.desc == "(Ljava/lang/Float;)F" - case Type.DOUBLE => insn.name == "Double2double" && insn.desc == "(Ljava/lang/Double;)D" - case _ => false - } - } - } - - def isRefCreate(insn: MethodInsnNode): Boolean = { - insn.name == "create" && { - val args = Type.getArgumentTypes(insn.desc) - args.length == 1 && ((args(0).getSort: @switch) match { - case Type.BOOLEAN => insn.owner == "scala/runtime/BooleanRef" && insn.desc == "(Z)Lscala/runtime/BooleanRef;" || insn.owner == "scala/runtime/VolatileBooleanRef" && insn.desc == "(Z)Lscala/runtime/VolatileBooleanRef;" - case Type.BYTE => insn.owner == "scala/runtime/ByteRef" && insn.desc == "(B)Lscala/runtime/ByteRef;" || insn.owner == "scala/runtime/VolatileByteRef" && insn.desc == "(B)Lscala/runtime/VolatileByteRef;" - case Type.CHAR => insn.owner == "scala/runtime/CharRef" && insn.desc == "(C)Lscala/runtime/CharRef;" || insn.owner == "scala/runtime/VolatileCharRef" && insn.desc == "(C)Lscala/runtime/VolatileCharRef;" - case Type.SHORT => insn.owner == "scala/runtime/ShortRef" && insn.desc == "(S)Lscala/runtime/ShortRef;" || insn.owner == "scala/runtime/VolatileShortRef" && insn.desc == "(S)Lscala/runtime/VolatileShortRef;" - case Type.INT => insn.owner == "scala/runtime/IntRef" && insn.desc == "(I)Lscala/runtime/IntRef;" || insn.owner == "scala/runtime/VolatileIntRef" && insn.desc == "(I)Lscala/runtime/VolatileIntRef;" - case Type.LONG => insn.owner == "scala/runtime/LongRef" && insn.desc == "(J)Lscala/runtime/LongRef;" || insn.owner == "scala/runtime/VolatileLongRef" && insn.desc == "(J)Lscala/runtime/VolatileLongRef;" - case Type.FLOAT => insn.owner == "scala/runtime/FloatRef" && insn.desc == "(F)Lscala/runtime/FloatRef;" || insn.owner == "scala/runtime/VolatileFloatRef" && insn.desc == "(F)Lscala/runtime/VolatileFloatRef;" - case Type.DOUBLE => insn.owner == "scala/runtime/DoubleRef" && insn.desc == "(D)Lscala/runtime/DoubleRef;" || insn.owner == "scala/runtime/VolatileDoubleRef" && insn.desc == "(D)Lscala/runtime/VolatileDoubleRef;" - case Type.OBJECT => insn.owner == "scala/runtime/ObjectRef" && insn.desc == "(Ljava/lang/Object;)Lscala/runtime/ObjectRef;" || insn.owner == "scala/runtime/VolatileObjectRef" && insn.desc == "(Ljava/lang/Object;)Lscala/runtime/VolatileObjectRef;" - case _ => false - }) - } - } - - private val jlObjectType = Type.getType("Ljava/lang/Object;") - - private val runtimeRefClassesAndTypes = Map( - ("scala/runtime/BooleanRef", Type.BOOLEAN_TYPE), - ("scala/runtime/ByteRef", Type.BYTE_TYPE), - ("scala/runtime/CharRef", Type.CHAR_TYPE), - ("scala/runtime/ShortRef", Type.SHORT_TYPE), - ("scala/runtime/IntRef", Type.INT_TYPE), - ("scala/runtime/LongRef", Type.LONG_TYPE), - ("scala/runtime/FloatRef", Type.FLOAT_TYPE), - ("scala/runtime/DoubleRef", Type.DOUBLE_TYPE), - ("scala/runtime/ObjectRef", jlObjectType), - ("scala/runtime/VolatileBooleanRef", Type.BOOLEAN_TYPE), - ("scala/runtime/VolatileByteRef", Type.BYTE_TYPE), - ("scala/runtime/VolatileCharRef", Type.CHAR_TYPE), - ("scala/runtime/VolatileShortRef", Type.SHORT_TYPE), - ("scala/runtime/VolatileIntRef", Type.INT_TYPE), - ("scala/runtime/VolatileLongRef", Type.LONG_TYPE), - ("scala/runtime/VolatileFloatRef", Type.FLOAT_TYPE), - ("scala/runtime/VolatileDoubleRef", Type.DOUBLE_TYPE), - ("scala/runtime/VolatileObjectRef", jlObjectType)) - - def isRefZero(insn: MethodInsnNode): Boolean = { - insn.name == "zero" && runtimeRefClassesAndTypes.contains(insn.owner) && insn.desc == "()L" + insn.owner + ";" - } - - def runtimeRefClassBoxedType(refClass: InternalName): Type = runtimeRefClassesAndTypes(refClass) - - def isModuleLoad(insn: AbstractInsnNode, moduleName: InternalName): Boolean = insn match { - case fi: FieldInsnNode => fi.getOpcode == GETSTATIC && fi.owner == moduleName && fi.name == "MODULE$" && fi.desc == ("L" + moduleName + ";") - case _ => false - } - - def isPredefLoad(insn: AbstractInsnNode) = isModuleLoad(insn, "scala/Predef$") - - private val primitiveBoxConstructors = Set( - "java/lang/Boolean(Z)V", - "java/lang/Byte(B)V", - "java/lang/Character(C)V", - "java/lang/Short(S)V", - "java/lang/Integer(I)V", - "java/lang/Long(J)V", - "java/lang/Float(F)V", - "java/lang/Double(D)V") - - def isPrimitiveBoxConstructor(insn: MethodInsnNode): Boolean = { - insn.name == INSTANCE_CONSTRUCTOR_NAME && primitiveBoxConstructors(insn.owner + insn.desc) - } - - private val runtimeRefConstructors = Set( - "scala/runtime/ObjectRef(Ljava/lang/Object;)V", - "scala/runtime/BooleanRef(Z)V", - "scala/runtime/ByteRef(B)V", - "scala/runtime/CharRef(C)V", - "scala/runtime/ShortRef(S)V", - "scala/runtime/IntRef(I)V", - "scala/runtime/LongRef(J)V", - "scala/runtime/FloatRef(F)V", - "scala/runtime/DoubleRef(D)V", - - "scala/runtime/VolatileObjectRef(Ljava/lang/Object;)V", - "scala/runtime/VolatileBooleanRef(Z)V", - "scala/runtime/VolatileByteRef(B)V", - "scala/runtime/VolatileCharRef(C)V", - "scala/runtime/VolatileShortRef(S)V", - "scala/runtime/VolatileIntRef(I)V", - "scala/runtime/VolatileLongRef(J)V", - "scala/runtime/VolatileFloatRef(F)V", - "scala/runtime/VolatileDoubleRef(D)V") - - def isRuntimeRefConstructor(insn: MethodInsnNode): Boolean = { - insn.name == INSTANCE_CONSTRUCTOR_NAME && runtimeRefConstructors(insn.owner + insn.desc) - } - - private val tupleConstructors = Set.empty[String] ++ { - (1 to 22).map(n => "scala/Tuple" + n + "(" + ("Ljava/lang/Object;" * n) + ")V") - } ++ { - Iterator("I", "J", "D").map(t => "scala/Tuple1$mc" + t + "$sp(" + t + ")V") - } ++ { - def tuple2Specs = Iterator("I", "J", "D", "C", "Z") - for (a <- tuple2Specs; b <- tuple2Specs) yield "scala/Tuple2$mc" + a + b + "$sp(" + a + b + ")V" - } - - def isTupleConstructor(insn: MethodInsnNode): Boolean = { - insn.name == INSTANCE_CONSTRUCTOR_NAME && tupleConstructors(insn.owner + insn.desc) - } - - // unused objects created by these constructors are eliminated by pushPop - private val sideEffectFreeConstructors = primitiveBoxConstructors ++ - runtimeRefConstructors ++ - tupleConstructors ++ Set( - "java/lang/Object()V", - "java/lang/String()V", - "java/lang/String(Ljava/lang/String;)V", - "java/lang/String([C)V" - ) - - def isSideEffectFreeConstructorCall(insn: MethodInsnNode): Boolean = { - insn.name == INSTANCE_CONSTRUCTOR_NAME && sideEffectFreeConstructors(insn.owner + insn.desc) - } - - private val classesForSideEffectFreeConstructors = sideEffectFreeConstructors.map(s => s.substring(0, s.indexOf('('))) - - // we only eliminate `NEW C` if the class C has a constructor that we consider side-effect free. - // removing a `NEW` eliminates a potential NoClassDefFoundError, so we only do it for core classes. - def isNewForSideEffectFreeConstructor(insn: AbstractInsnNode) = { - insn.getOpcode == NEW && { - val ti = insn.asInstanceOf[TypeInsnNode] - classesForSideEffectFreeConstructors(ti.desc) - } - } - - def isBoxedUnit(insn: AbstractInsnNode) = { - insn.getOpcode == GETSTATIC && { - val fi = insn.asInstanceOf[FieldInsnNode] - fi.owner == "scala/runtime/BoxedUnit" && fi.name == "UNIT" && fi.desc == "Lscala/runtime/BoxedUnit;" - } - } - - private def buildFunctionTypes(base: String): Set[InternalName] = { - def primitives = Iterator("B", "S", "I", "J", "C", "F", "D", "Z", "V") - def ijfd = Iterator("I", "J", "F", "D") - def ijfdzv = Iterator("I", "J", "F", "D", "Z", "V") - def ijd = Iterator("I", "J", "D") - Set.empty[String] ++ { - (0 to 22).map(base + _) - } ++ { - primitives.map(base + "0$mc" + _ + "$sp") // Function0 - } ++ { - // return type specializations appear first in the name string (alphabetical sorting) - for (r <- ijfdzv; a <- ijfd) yield base + "1$mc" + r + a + "$sp" // Function1 - } ++ { - for (r <- ijfdzv; a <- ijd; b <- ijd) yield base + "2$mc" + r + a + b + "$sp" // Function2 - } - } - - private val srJFunctionTypes: Set[InternalName] = buildFunctionTypes("scala/runtime/java8/JFunction") - def isrJFunctionType(internalName: InternalName): Boolean = srJFunctionTypes(internalName) - - private val sFunctionTypes: Set[InternalName] = buildFunctionTypes("scala/Function") - def isScalaFunctionType(internalName: InternalName): Boolean = sFunctionTypes(internalName) - implicit class AnalyzerExtensions[V <: Value](val analyzer: Analyzer[V]) extends AnyVal { def frameAt(instruction: AbstractInsnNode, methodNode: MethodNode): Frame[V] = analyzer.getFrames()(methodNode.instructions.indexOf(instruction)) } diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala index 64677ddcc0..863bb2d10a 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CallGraph.scala @@ -103,7 +103,7 @@ class CallGraph[BT <: BTypes](val btypes: BT) { val analyzer = { if (compilerSettings.YoptNullnessTracking && AsmAnalyzer.sizeOKForNullness(methodNode)) { - Some(new AsmAnalyzer(methodNode, definingClass.internalName, new NullnessAnalyzer)) + Some(new AsmAnalyzer(methodNode, definingClass.internalName, new NullnessAnalyzer(btypes))) } else if (AsmAnalyzer.sizeOKForBasicValue(methodNode)) { Some(new AsmAnalyzer(methodNode, definingClass.internalName)) } else None diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala index 35bc82d098..f98a08a6a9 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/ClosureOptimizer.scala @@ -254,18 +254,6 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { Type.VOID <= sort && sort <= Type.DOUBLE } - private def unboxOp(primitiveType: Type): MethodInsnNode = { - val bType = bTypeForDescriptorOrInternalNameFromClassfile(primitiveType.getDescriptor) - val MethodNameAndType(name, methodBType) = asmUnboxTo(bType) - new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) - } - - private def boxOp(primitiveType: Type): MethodInsnNode = { - val bType = bTypeForDescriptorOrInternalNameFromClassfile(primitiveType.getDescriptor) - val MethodNameAndType(name, methodBType) = asmBoxTo(bType) - new MethodInsnNode(INVOKESTATIC, srBoxesRunTimeRef.internalName, name, methodBType.descriptor, /*itf =*/ false) - } - /** * The argument types of the lambda body method may differ in two ways from the argument types of * the closure member method that is invoked (and replaced by a call to the body). @@ -291,9 +279,9 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { if (invokeArgTypes(i) == implMethodArgTypes(i)) { res(i) = None } else if (isPrimitiveType(implMethodArgTypes(i)) && invokeArgTypes(i).getDescriptor == ObjectRef.descriptor) { - res(i) = Some(unboxOp(implMethodArgTypes(i))) + res(i) = Some(getScalaUnbox(implMethodArgTypes(i))) } else if (isPrimitiveType(invokeArgTypes(i)) && implMethodArgTypes(i).getDescriptor == ObjectRef.descriptor) { - res(i) = Some(boxOp(invokeArgTypes(i))) + res(i) = Some(getScalaBox(invokeArgTypes(i))) } else { assert(!isPrimitiveType(invokeArgTypes(i)), invokeArgTypes(i)) assert(!isPrimitiveType(implMethodArgTypes(i)), implMethodArgTypes(i)) @@ -354,12 +342,12 @@ class ClosureOptimizer[BT <: BTypes](val btypes: BT) { if (isPrimitiveType(invocationReturnType) && bodyReturnType.getDescriptor == ObjectRef.descriptor) { val op = if (invocationReturnType.getSort == Type.VOID) getPop(1) - else unboxOp(invocationReturnType) + else getScalaUnbox(invocationReturnType) ownerMethod.instructions.insertBefore(invocation, op) } else if (isPrimitiveType(bodyReturnType) && invocationReturnType.getDescriptor == ObjectRef.descriptor) { val op = if (bodyReturnType.getSort == Type.VOID) getBoxedUnit - else boxOp(bodyReturnType) + else getScalaBox(bodyReturnType) ownerMethod.instructions.insertBefore(invocation, op) } else { // see comment of that method diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 8db922a635..cd4d5be197 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -364,7 +364,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { */ def nullnessOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = { AsmAnalyzer.sizeOKForNullness(method) && { - lazy val nullnessAnalyzer = new AsmAnalyzer(method, ownerClassName, new NullnessAnalyzer) + lazy val nullnessAnalyzer = new AsmAnalyzer(method, ownerClassName, new NullnessAnalyzer(btypes)) // When running nullness optimizations the method may still have unreachable code. Analyzer // frames of unreachable instructions are `null`. diff --git a/src/compiler/scala/tools/nsc/transform/LambdaLift.scala b/src/compiler/scala/tools/nsc/transform/LambdaLift.scala index d1be1558b9..43f698d7ba 100644 --- a/src/compiler/scala/tools/nsc/transform/LambdaLift.scala +++ b/src/compiler/scala/tools/nsc/transform/LambdaLift.scala @@ -31,11 +31,6 @@ abstract class LambdaLift extends InfoTransform { } } - /** scala.runtime.*Ref classes */ - private lazy val allRefClasses: Set[Symbol] = { - refClass.values.toSet ++ volatileRefClass.values.toSet ++ Set(VolatileObjectRefClass, ObjectRefClass) - } - /** Each scala.runtime.*Ref class has a static method `create(value)` that simply instantiates the Ref to carry that value. */ private lazy val refCreateMethod: Map[Symbol, Symbol] = { mapFrom(allRefClasses.toList)(x => getMemberMethod(x.companionModule, nme.create)) diff --git a/src/reflect/scala/reflect/internal/Definitions.scala b/src/reflect/scala/reflect/internal/Definitions.scala index 5ce5c39145..f2da7614a3 100644 --- a/src/reflect/scala/reflect/internal/Definitions.scala +++ b/src/reflect/scala/reflect/internal/Definitions.scala @@ -93,6 +93,10 @@ trait Definitions extends api.StandardDefinitions { lazy val refClass = classesMap(x => getRequiredClass("scala.runtime." + x + "Ref")) lazy val volatileRefClass = classesMap(x => getRequiredClass("scala.runtime.Volatile" + x + "Ref")) + lazy val allRefClasses: Set[Symbol] = { + refClass.values.toSet ++ volatileRefClass.values.toSet ++ Set(VolatileObjectRefClass, ObjectRefClass) + } + def isNumericSubClass(sub: Symbol, sup: Symbol) = ( (numericWeight contains sub) && (numericWeight contains sup) diff --git a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala index 0132fff17c..46e4371cfa 100644 --- a/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala +++ b/src/reflect/scala/reflect/runtime/JavaUniverseForce.scala @@ -424,6 +424,7 @@ trait JavaUniverseForce { self: runtime.JavaUniverse => definitions.boxedClass definitions.refClass definitions.volatileRefClass + definitions.allRefClasses definitions.UnitClass definitions.ByteClass definitions.ShortClass diff --git a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala index a7d1dc168a..daff6fc223 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/analysis/NullnessAnalyzerTest.scala @@ -33,7 +33,7 @@ class NullnessAnalyzerTest extends ClearAfterClass { val noOptCompiler = NullnessAnalyzerTest.noOptCompiler import noOptCompiler.genBCode.bTypes.backendUtils._ - def newNullnessAnalyzer(methodNode: MethodNode, classInternalName: InternalName = "C") = new AsmAnalyzer(methodNode, classInternalName, new NullnessAnalyzer) + def newNullnessAnalyzer(methodNode: MethodNode, classInternalName: InternalName = "C") = new AsmAnalyzer(methodNode, classInternalName, new NullnessAnalyzer(noOptCompiler.genBCode.bTypes)) def testNullness(analyzer: AsmAnalyzer[NullnessValue], method: MethodNode, query: String, index: Int, nullness: NullnessValue): Unit = { for (i <- findInstr(method, query)) { -- cgit v1.2.3 From 790ed59d9fe5ca396984b8a5135b7a091e4224f7 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Sat, 23 Jan 2016 21:03:10 +0100 Subject: SI-8601 Don't treat newarray as dead code Otherwise we lose the side effect of a `NegativeArraySizeException`. A test for this case already exists (run/t8601b.scala), but it currently enforces `-optimize -Ybackend:GenASM`, so it didn't trigger on the new backend. However, PR #4814 was merged into 2.12.x and moved that test over to the new backend and optimizer. After merging the 2.12.x into the current optimizer branch (push-pop elimination), the test started failing. Also disable the optimizer for `jvm/bytecode-test-example`: it counts the number of null checks in a method, the optimizer (rightly) eliminates one of the two. --- src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala | 4 ---- test/files/jvm/bytecode-test-example.flags | 1 + test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala | 4 ++-- 3 files changed, 3 insertions(+), 6 deletions(-) create mode 100644 test/files/jvm/bytecode-test-example.flags diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala index 149742888a..f91530903d 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/CopyProp.scala @@ -382,10 +382,6 @@ class CopyProp[BT <: BTypes](val btypes: BT) { popAfterProd() } - case NEWARRAY | ANEWARRAY => - toRemove += prod - handleInputs(prod, 1) - case MULTIANEWARRAY => toRemove += prod handleInputs(prod, prod.asInstanceOf[MultiANewArrayInsnNode].dims) diff --git a/test/files/jvm/bytecode-test-example.flags b/test/files/jvm/bytecode-test-example.flags new file mode 100644 index 0000000000..bc22511cff --- /dev/null +++ b/test/files/jvm/bytecode-test-example.flags @@ -0,0 +1 @@ +-Yopt:l:none diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala index 5ee52ff78f..ec063c6cb3 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -161,7 +161,7 @@ class MethodLevelOptsTest extends ClearAfterClass { | val b = (a, y) // Tuple2 | val c = (new Object, "krik", new String) // unused java/lang/Object, java/lang/String allocation and string constant is also eliminated | val d = new java.lang.Integer(x) - | val e = new String(new Array[Char](23)) + | val e = new String(new Array[Char](23)) // array allocation not eliminated, as it may throw (negative size, SI-8601) | val f = new scala.runtime.IntRef(11) | x + y | } @@ -169,7 +169,7 @@ class MethodLevelOptsTest extends ClearAfterClass { """.stripMargin val List(c) = compileClasses(methodOptCompiler)(code) assertEquals(getSingleMethod(c, "t").instructions.dropNonOp, - List(VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + List(IntOp(BIPUSH, 23), IntOp(NEWARRAY, 5), Op(POP), VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) } @Test -- cgit v1.2.3 From 1a9e649e34cda4306537d74ab425c33d5f6a77db Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Sun, 24 Jan 2016 10:19:25 +0100 Subject: Jump optimizations may enable more push-pop elimination Jump optimization replaces an unnecessary conditional jump, e.g. `IFNULL l; l: ...` by `POP`, which enables further push-pop elimination. Also introduces a `-YoptTrace` flag that traces the progress of the bytecode as it goes through local optimizations. --- .../scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 34 ++++++++++++++++++++-- .../scala/tools/nsc/settings/ScalaSettings.scala | 2 ++ .../nsc/backend/jvm/opt/MethodLevelOptsTest.scala | 16 ++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index cd4d5be197..7a1dcd9ec6 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -104,6 +104,7 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * + enables UPSTREAM * - unreachable code (`GOTO a; a: GOTO b; b: ...`, the first jump is changed to `GOTO b`, the second becomes unreachable) * - store-load pairs (a `GOTO l; l: ...` is removed between store and load) + * - push-pop (`IFNULL l; l: ...` is replaced by `POP`) * * * The following cleanup optimizations don't enable any upstream optimizations, so they can be @@ -228,6 +229,16 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // This triggers "ClassFormatError: Illegal exception table range in class file C". Similar // for local variables in dead blocks. Maybe that's a bug in the ASM framework. + var currentTrace: String = null + def traceIfChanged(optName: String): Unit = if (compilerSettings.YoptTrace.value) { + val after = AsmUtils.textify(method) + if (currentTrace != after) { + println(s"after $optName") + println(after) + } + currentTrace = after + } + /** * Runs the optimizations that depend on each other in a loop until reaching a fixpoint. See * comment in class [[LocalOpt]]. @@ -239,14 +250,18 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { requestDCE: Boolean, requestBoxUnbox: Boolean, requestStaleStores: Boolean, + requestPushPop: Boolean, requestStoreLoad: Boolean, firstIteration: Boolean, maxRecursion: Int = 10): (Boolean, Boolean) = { if (maxRecursion == 0) return (false, false) + traceIfChanged("beforeMethodOpt") + // NULLNESS OPTIMIZATIONS val runNullness = compilerSettings.YoptNullnessTracking && requestNullness val nullnessOptChanged = runNullness && nullnessOptimizations(method, ownerClassName) + traceIfChanged("nullness") // UNREACHABLE CODE // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores, @@ -255,54 +270,65 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { compilerSettings.YoptBoxUnbox || compilerSettings.YoptCopyPropagation val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) + traceIfChanged("dce") // BOX-UNBOX val runBoxUnbox = compilerSettings.YoptBoxUnbox && (requestBoxUnbox || nullnessOptChanged) val boxUnboxChanged = runBoxUnbox && boxUnboxElimination(method, ownerClassName) + traceIfChanged("boxUnbox") // COPY PROPAGATION val runCopyProp = compilerSettings.YoptCopyPropagation && (firstIteration || boxUnboxChanged) val copyPropChanged = runCopyProp && copyPropagation(method, ownerClassName) + traceIfChanged("copyProp") // STALE STORES val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || nullnessOptChanged || codeRemoved || boxUnboxChanged || copyPropChanged) val storesRemoved = runStaleStores && eliminateStaleStores(method, ownerClassName) + traceIfChanged("staleStores") // REDUNDANT CASTS val runRedundantCasts = compilerSettings.YoptRedundantCasts && (firstIteration || boxUnboxChanged) val castRemoved = runRedundantCasts && eliminateRedundantCasts(method, ownerClassName) + traceIfChanged("redundantCasts") // PUSH-POP - val runPushPop = compilerSettings.YoptCopyPropagation && (firstIteration || storesRemoved || castRemoved) + val runPushPop = compilerSettings.YoptCopyPropagation && (requestPushPop || firstIteration || storesRemoved || castRemoved) val pushPopRemoved = runPushPop && eliminatePushPop(method, ownerClassName) + traceIfChanged("pushPop") // STORE-LOAD PAIRS val runStoreLoad = compilerSettings.YoptCopyPropagation && (requestStoreLoad || boxUnboxChanged || copyPropChanged || pushPopRemoved) val storeLoadRemoved = runStoreLoad && eliminateStoreLoad(method) + traceIfChanged("storeLoadPairs") // STALE HANDLERS val removedHandlers = if (runDCE) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] val handlersRemoved = removedHandlers.nonEmpty val liveHandlerRemoved = removedHandlers.exists(h => liveLabels(h.start)) + traceIfChanged("staleHandlers") // SIMPLIFY JUMPS // almost all of the above optimizations enable simplifying more jumps, so we just run it in every iteration val runSimplifyJumps = compilerSettings.YoptSimplifyJumps val jumpsChanged = runSimplifyJumps && simplifyJumps(method) + traceIfChanged("simplifyJumps") // See doc comment in the beginning of this file (optimizations marked UPSTREAM) val runNullnessAgain = boxUnboxChanged val runDCEAgain = liveHandlerRemoved || jumpsChanged val runBoxUnboxAgain = boxUnboxChanged || castRemoved || pushPopRemoved || liveHandlerRemoved val runStaleStoresAgain = pushPopRemoved + val runPushPopAgain = jumpsChanged val runStoreLoadAgain = jumpsChanged - val runAgain = runNullnessAgain || runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runStoreLoadAgain + val runAgain = runNullnessAgain || runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runPushPopAgain || runStoreLoadAgain val downstreamRequireEliminateUnusedLocals = runAgain && removalRound( requestNullness = runNullnessAgain, requestDCE = runDCEAgain, requestBoxUnbox = runBoxUnboxAgain, requestStaleStores = runStaleStoresAgain, + requestPushPop = runPushPopAgain, requestStoreLoad = runStoreLoadAgain, firstIteration = false, maxRecursion = maxRecursion - 1)._2 @@ -327,6 +353,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { requestDCE = true, requestBoxUnbox = true, requestStaleStores = true, + requestPushPop = true, requestStoreLoad = true, firstIteration = true) if (compilerSettings.YoptUnreachableCode) unreachableCodeEliminated += method @@ -338,10 +365,13 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { if (compilerSettings.YoptCompactLocals) compactLocalVariables(method) // also removes unused else if (requireEliminateUnusedLocals) removeUnusedLocalVariableNodes(method)() // (*) else false + traceIfChanged("localVariables") val lineNumbersRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLineNumbers(method) else false + traceIfChanged("lineNumbers") val labelsRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLabelNodes(method) else false + traceIfChanged("labels") // assert that local variable annotations are empty (we don't emit them) - otherwise we'd have // to eliminate those covering an empty range, similar to removeUnusedLocalVariableNodes. diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index f64fe437e4..1bb1943c0c 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -322,6 +322,8 @@ trait ScalaSettings extends AbsScalaSettings def YoptWarningNoInlineMissingBytecode = YoptWarnings.contains(YoptWarningsChoices.noInlineMissingBytecode) def YoptWarningNoInlineMissingScalaInlineInfoAttr = YoptWarnings.contains(YoptWarningsChoices.noInlineMissingScalaInlineInfoAttr) + val YoptTrace = BooleanSetting("-YoptTrace", "trace optimizer progress") + private def removalIn212 = "This flag is scheduled for removal in 2.12. If you have a case where you need this flag then please report a bug." object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup, jvm = Value } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala index ec063c6cb3..46e1cebfc4 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -543,4 +543,20 @@ class MethodLevelOptsTest extends ClearAfterClass { assertSameCode(getSingleMethod(c, "t6").instructions.dropNonOp, List(Op(ACONST_NULL), Op(ARETURN))) assertSameCode(getSingleMethod(c, "t7").instructions.dropNonOp, List(Op(ICONST_0), Op(IRETURN))) } + + @Test + def elimRedundantNullCheck(): Unit = { + val code = + """class C { + | def t(x: Object) = { + | val bool = x == null + | if (x != null) 1 else 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode( + getSingleMethod(c, "t").instructions.dropNonOp, + List(VarOp(ALOAD, 1), Jump(IFNULL, Label(6)), Op(ICONST_1), Op(IRETURN), Label(6), Op(ICONST_0), Op(IRETURN))) + } } -- cgit v1.2.3 From 3dccf0ee22303e1c192ed3e40391df48d811f3f6 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Mon, 25 Jan 2016 09:52:05 +0100 Subject: -YoptTrace takes a package/Class.method parameter --- src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 3 ++- src/compiler/scala/tools/nsc/settings/ScalaSettings.scala | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 7a1dcd9ec6..c1fdb7eb59 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -230,7 +230,8 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // for local variables in dead blocks. Maybe that's a bug in the ASM framework. var currentTrace: String = null - def traceIfChanged(optName: String): Unit = if (compilerSettings.YoptTrace.value) { + val doTrace = compilerSettings.YoptTrace.isSetByUser && compilerSettings.YoptTrace.value == ownerClassName + "." + method.name + def traceIfChanged(optName: String): Unit = if (doTrace) { val after = AsmUtils.textify(method) if (currentTrace != after) { println(s"after $optName") diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala index d6ca0125ea..20e2c4346f 100644 --- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala +++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala @@ -319,7 +319,7 @@ trait ScalaSettings extends AbsScalaSettings def YoptWarningNoInlineMissingBytecode = YoptWarnings.contains(YoptWarningsChoices.noInlineMissingBytecode) def YoptWarningNoInlineMissingScalaInlineInfoAttr = YoptWarnings.contains(YoptWarningsChoices.noInlineMissingScalaInlineInfoAttr) - val YoptTrace = BooleanSetting("-YoptTrace", "trace optimizer progress") + val YoptTrace = StringSetting("-YoptTrace", "package/Class.method", "Trace the optimizer progress for a specific method.", "") private def removalIn212 = "This flag is scheduled for removal in 2.12. If you have a case where you need this flag then please report a bug." -- cgit v1.2.3