summaryrefslogtreecommitdiff
path: root/src/compiler
diff options
context:
space:
mode:
authorLukas Rytz <lukas.rytz@gmail.com>2015-12-11 10:47:00 +0100
committerLukas Rytz <lukas.rytz@gmail.com>2015-12-15 15:12:42 +0100
commit1265e19de8073da2691fac4d52bc763091ad7b9c (patch)
tree74f7183a2e41ed2f1daf28751b75025d0fe1574b /src/compiler
parent60ac9ecdbd9584007d70003bf8e00c4702bbd401 (diff)
downloadscala-1265e19de8073da2691fac4d52bc763091ad7b9c.tar.gz
scala-1265e19de8073da2691fac4d52bc763091ad7b9c.tar.bz2
scala-1265e19de8073da2691fac4d52bc763091ad7b9c.zip
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.
Diffstat (limited to 'src/compiler')
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/BoxUnbox.scala902
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/BytecodeUtils.scala206
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala146
-rw-r--r--src/compiler/scala/tools/nsc/settings/ScalaSettings.scala4
4 files changed, 1184 insertions, 74 deletions
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)