/* 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 import scala.tools.asm.Opcodes import scala.tools.asm.tree.analysis.{Analyzer, BasicInterpreter} import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.BTypes.InternalName import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ /** * Optimizations within a single method. * * 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` * * 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. * * 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" * * 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) * * 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 labels * - eliminate labels that are not referenced, merge sequences of label definitions. */ class LocalOpt[BT <: BTypes](val btypes: BT) { import LocalOptImpls._ import btypes._ /** * Remove unreachable code from a method. * * This implementation only removes instructions that are unreachable for an ASM analyzer / * interpreter. This ensures that future analyses will not produce `null` frames. The inliner * and call graph builder depend on this property. * * @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 // 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) { val liveHandlerRemoved = removeEmptyExceptionHandlers(method).exists(h => liveLabels(h.start)) if (liveHandlerRemoved) removalRound() else Set.empty } else Set.empty removedInstructions ++ removedRecursively } val removedInstructions = removalRound() if (removedInstructions.nonEmpty) removeUnusedLocalVariableNodes(method)() unreachableCodeEliminated += method removedInstructions } /** * Remove unreachable instructions from all (non-abstract) methods and apply various other * cleanups to the bytecode. * * @param clazz The class whose methods are optimized * @return `true` if unreachable code was eliminated in some method, `false` otherwise. */ def methodOptimizations(clazz: ClassNode): Boolean = { !compilerSettings.YoptNone && clazz.methods.asScala.foldLeft(false) { case (changed, method) => methodOptimizations(method, clazz.name) || changed } } /** * 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. * * Returns `true` if the bytecode of `method` was changed. */ def methodOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = { if (method.instructions.size == 0) return false // fast path for abstract methods // unreachable-code also removes unused local variable nodes and empty exception handlers. // This is required for correctness, for example: // // def f = { return 0; try { 1 } catch { case _ => 2 } } // // The result after removeUnreachableCodeImpl: // // TRYCATCHBLOCK L0 L1 L2 java/lang/Exception // L4 // ICONST_0 // IRETURN // L0 // L1 // L2 // // If we don't eliminate the handler, the ClassWriter emits: // // TRYCATCHBLOCK L0 L0 L0 java/lang/Exception // L1 // ICONST_0 // IRETURN // L0 // // 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) = if (compilerSettings.YoptUnreachableCode) { val (removedInstructions, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) val removedHandlers = removeEmptyExceptionHandlers(method) (removedInstructions.nonEmpty, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) } else { (false, false, false) } 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() codeRemoved || handlersRemoved || jumpsChanged } val codeHandlersOrJumpsChanged = removalRound() // (*) Removing stale local variable descriptors is required for correctness of unreachable-code val localsRemoved = if (compilerSettings.YoptCompactLocals) compactLocalVariables(method) // also removes unused else if (compilerSettings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*) else false val lineNumbersRemoved = if (compilerSettings.YoptEmptyLineNumbers) removeEmptyLineNumbers(method) else false val labelsRemoved = if (compilerSettings.YoptEmptyLabels) 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. def nullOrEmpty[T](l: java.util.List[T]) = l == null || l.isEmpty assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations) assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) unreachableCodeEliminated += method codeHandlersOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved } } object LocalOptImpls { /** * Removes unreachable basic blocks. * * TODO: rewrite, don't use computeMaxLocalsMaxStack (runs a ClassWriter) / Analyzer. Too slow. * * @return A set containing eliminated instructions, and a set containing all live label nodes. */ def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Set[AbstractInsnNode], Set[LabelNode]) = { // The data flow analysis requires the maxLocals / maxStack fields of the method to be computed. computeMaxLocalsMaxStack(method) val a = new Analyzer(new BasicInterpreter) a.analyze(ownerClassName, method) val frames = a.getFrames val initialSize = method.instructions.size var i = 0 var liveLabels = Set.empty[LabelNode] var removedInstructions = Set.empty[AbstractInsnNode] val itr = method.instructions.iterator() while (itr.hasNext) { itr.next() match { case l: LabelNode => if (frames(i) != null) liveLabels += l case ins => // label nodes are not removed: they might be referenced for example in a LocalVariableNode if (frames(i) == null || ins.getOpcode == Opcodes.NOP) { // Instruction iterators allow removing during iteration. // Removing is O(1): instructions are doubly linked list elements. itr.remove() removedInstructions += ins } } i += 1 } (removedInstructions, liveLabels) } /** * Remove exception handlers that cover empty code blocks. A block is considered empty if it * consist only of labels, frames, line numbers, nops and gotos. * * There are no executable instructions that we can assume don't throw (eg ILOAD). The JVM spec * basically says that a VirtualMachineError may be thrown at any time: * http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.3 * * Note that no instructions are eliminated. * * @return the set of removed handlers */ 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 { // FrameNode, LabelNode and LineNumberNode have opcode == -1. case -1 | Opcodes.GOTO => containsExecutableCode(start.getNext, end) case _ => true }) } var removedHandlers = Set.empty[TryCatchBlockNode] val handlersIter = method.tryCatchBlocks.iterator() while(handlersIter.hasNext) { val handler = handlersIter.next() if (!containsExecutableCode(handler.start, handler.end)) { removedHandlers += handler handlersIter.remove() } } removedHandlers } /** * Remove all non-parameter entries from the local variable table which denote variables that are * not actually read or written. * * Note that each entry in the local variable table has a start, end and index. Two entries with * the same index, but distinct start / end ranges are different variables, they may have not the * 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 = { start != end && (start match { case v: VarInsnNode if v.`var` == varIndex => true case _ => variableIsUsed(start.getNext, end, varIndex) }) } val initialNumVars = method.localVariables.size val localsIter = method.localVariables.iterator() while (localsIter.hasNext) { val local = localsIter.next() val index = local.index // parameters and `this` (the lowest indices, starting at 0) are never removed or renumbered if (index >= firstLocalIndex) { if (!variableIsUsed(local.start, local.end, index)) localsIter.remove() else if (renumber(index) != index) local.index = renumber(index) } } 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. * * This transformation reduces the size of the frame for invoking the method. For example, if the * method has an ISTORE instruction to the local variable 3, the maxLocals of the method is at * least 4, even if some local variable slots below 3 are not used by any instruction. * * This could be improved by doing proper register allocation. */ def compactLocalVariables(method: MethodNode): Boolean = { // This array is built up to map local variable indices from old to new. 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` val isWide = (varIns.getOpcode: @switch) match { case Opcodes.LLOAD | Opcodes.DLOAD | Opcodes.LSTORE | Opcodes.DSTORE => true case _ => false } // Ensure the length of `renumber`. Unused variable indices are mapped to -1. val minLength = if (isWide) index + 2 else index + 1 for (i <- renumber.length until minLength) renumber += -1 renumber(index) = index if (isWide) renumber(index + 1) = index } // first phase: collect all used local variables. if the variable at index x is used, set // renumber(x) = x, otherwise renumber(x) = -1. if the variable is wide (long or double), set // renumber(x+1) = x. 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 _ => } // assign the next free slot to each used local variable. // for example, rewrite (0, 1, -1, 3, -1, 5) to (0, 1, -1, 2, -1, 3). var nextIndex = firstLocalIndex for (i <- firstLocalIndex until renumber.length if renumber(i) != -1) { renumber(i) = nextIndex nextIndex += 1 } // Update the local variable descriptors according to the renumber table, and eliminate stale entries val removedLocalVariableDescriptors = removeUnusedLocalVariableNodes(method)(firstLocalIndex, renumber) if (nextIndex == renumber.length) removedLocalVariableDescriptors else { // 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 _ => } true } } /** * Removes LineNumberNodes that don't describe any executable instructions. * * This method expects (and asserts) that the `start` label of each LineNumberNode is the * lexically preceding label declaration. */ def removeEmptyLineNumbers(method: MethodNode): Boolean = { def isEmpty(node: AbstractInsnNode): Boolean = node.getNext match { case null => true case l: LineNumberNode => true case n if n.getOpcode >= 0 => false case n => isEmpty(n) } val initialSize = method.instructions.size val iterator = method.instructions.iterator() var previousLabel: LabelNode = null while (iterator.hasNext) { iterator.next match { case label: LabelNode => previousLabel = label case line: LineNumberNode if isEmpty(line) => assert(line.start == previousLabel) iterator.remove() case _ => } } method.instructions.size != initialSize } /** * Removes unreferenced label declarations, also squashes sequences of label definitions. * * [ops]; Label(a); Label(b); [ops]; * => subs([ops], b, a); Label(a); subs([ops], b, a); */ def removeEmptyLabelNodes(method: MethodNode): Boolean = { val references = labelReferences(method) val initialSize = method.instructions.size val iterator = method.instructions.iterator() var prev: LabelNode = null while (iterator.hasNext) { iterator.next match { case label: LabelNode => if (!references.contains(label)) iterator.remove() else if (prev != null) { references(label).foreach(substituteLabel(_, label, prev)) iterator.remove() } else prev = label case instruction => if (instruction.getOpcode >= 0) prev = null } } method.instructions.size != initialSize } /** * Apply various simplifications to branching instructions. */ def simplifyJumps(method: MethodNode): Boolean = { var changed = false val allHandlers = method.tryCatchBlocks.asScala.toSet // A set of all exception handlers that guard the current instruction, required for simplifyGotoReturn var activeHandlers = Set.empty[TryCatchBlockNode] // Instructions that need to be removed. simplifyBranchOverGoto returns an instruction to be // removed. It cannot remove it itself because the instruction may be the successor of the current // instruction of the iterator, which is not supported in ASM. var instructionsToRemove = Set.empty[AbstractInsnNode] val iterator = method.instructions.iterator() while (iterator.hasNext) { val instruction = iterator.next() instruction match { case l: LabelNode => activeHandlers ++= allHandlers.filter(_.start == l) activeHandlers = activeHandlers.filter(_.end != l) case _ => } if (instructionsToRemove(instruction)) { iterator.remove() instructionsToRemove -= instruction } else if (isJumpNonJsr(instruction)) { // fast path - all of the below only treat jumps var jumpRemoved = simplifyThenElseSameTarget(method, instruction) if (!jumpRemoved) { changed = collapseJumpChains(instruction) || changed jumpRemoved = removeJumpToSuccessor(method, instruction) if (!jumpRemoved) { val staleGoto = simplifyBranchOverGoto(method, instruction) instructionsToRemove ++= staleGoto changed ||= staleGoto.nonEmpty changed = simplifyGotoReturn(method, instruction, inTryBlock = activeHandlers.nonEmpty) || changed } } changed ||= jumpRemoved } } assert(instructionsToRemove.isEmpty, "some optimization required removing a previously traversed instruction. add `instructionsToRemove.foreach(method.instructions.remove)`") changed } /** * Removes a conditional jump if it is followed by a GOTO to the same destination. * * CondJump l; [nops]; GOTO l; [...] * POP*; [nops]; GOTO l; [...] * * Introduces 1 or 2 POP instructions, depending on the number of values consumed by the CondJump. */ private def simplifyThenElseSameTarget(method: MethodNode, instruction: AbstractInsnNode): Boolean = instruction match { case ConditionalJump(jump) => nextExecutableInstruction(instruction) match { case Some(Goto(elseJump)) if sameTargetExecutableInstruction(jump, elseJump) => removeJumpAndAdjustStack(method, jump) true case _ => false } case _ => false } /** * Replace jumps to a sequence of GOTO instructions by a jump to the final destination. * * Jump l; [any ops]; l: GOTO m; [any ops]; m: GOTO n; [any ops]; n: NotGOTO; [...] * => Jump n; [rest unchanged] * * If there's a loop of GOTOs, the initial jump is replaced by one of the labels in the loop. */ private def collapseJumpChains(instruction: AbstractInsnNode): Boolean = instruction match { case JumpNonJsr(jump) => val target = finalJumpTarget(jump) if (jump.label == target) false else { jump.label = target true } case _ => false } /** * Eliminates unnecessary jump instructions * * Jump l; [nops]; l: [...] * => POP*; [nops]; l: [...] * * Introduces 0, 1 or 2 POP instructions, depending on the number of values consumed by the Jump. */ private def removeJumpToSuccessor(method: MethodNode, instruction: AbstractInsnNode) = instruction match { case JumpNonJsr(jump) if nextExecutableInstruction(jump, alsoKeep = Set(jump.label)) == Some(jump.label) => removeJumpAndAdjustStack(method, jump) true case _ => false } /** * If the "else" part of a conditional branch is a simple GOTO, negates the conditional branch * and eliminates the GOTO. * * CondJump l; [nops, no labels]; GOTO m; [nops]; l: [...] * => NegatedCondJump m; [nops, no labels]; [nops]; l: [...] * * Note that no label definitions are allowed in the first [nops] section. Otherwise, there could * be some other jump to the GOTO, and eliminating it would change behavior. * * For technical reasons, we cannot remove the GOTO here (*).Instead this method returns an Option * containing the GOTO that needs to be eliminated. * * (*) The ASM instruction iterator (used in the caller [[simplifyJumps]]) has an undefined * behavior if the successor of the current instruction is removed, which may be the case here */ 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 { case Some(Goto(goto)) => if (nextExecutableInstruction(goto, alsoKeep = Set(jump.label)) == Some(jump.label)) { val newJump = new JumpInsnNode(negateJumpOpcode(jump.getOpcode), goto.label) method.instructions.set(jump, newJump) Some(goto) } else None case _ => None } case _ => None } /** * Inlines xRETURN and ATHROW * * GOTO l; [any ops]; l: xRETURN/ATHROW * => xRETURN/ATHROW; [any ops]; l: xRETURN/ATHROW * * inlining is only done if the GOTO instruction is not part of a try block, otherwise the * rewrite might change the behavior. For xRETURN, the reason is that return instructions may throw * an IllegalMonitorStateException, as described here: * http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.return */ private def simplifyGotoReturn(method: MethodNode, instruction: AbstractInsnNode, inTryBlock: Boolean): Boolean = !inTryBlock && (instruction match { case Goto(jump) => nextExecutableInstruction(jump.label) match { case Some(target) => if (isReturn(target) || target.getOpcode == Opcodes.ATHROW) { method.instructions.set(jump, target.clone(null)) true } else false case _ => false } case _ => false }) }