From 6fbf9092f99c011be49dbe0e3e84c545f20ddffc Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Tue, 31 Oct 2017 11:01:06 +0100 Subject: [PATCH 1/5] Import backend/LocalOpt.scala Changes on these files should instead be made on the forked repo, this is just for experimentation. --- .../tools/backend/jvm/opt/BytecodeUtils.scala | 184 ++++++ .../tools/backend/jvm/opt/LocalOpt.scala | 562 ++++++++++++++++++ 2 files changed, 746 insertions(+) create mode 100644 compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala create mode 100644 compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala diff --git a/compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala b/compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala new file mode 100644 index 000000000000..6b4047c0a786 --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala @@ -0,0 +1,184 @@ +/* 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, switch} +import scala.collection.mutable +import scala.reflect.internal.util.Collections._ +import scala.tools.asm.Opcodes +import scala.tools.asm.tree._ +import scala.collection.convert.decorateAsScala._ + +object BytecodeUtils { + + object Goto { + def unapply(instruction: AbstractInsnNode): Option[JumpInsnNode] = { + if (instruction.getOpcode == Opcodes.GOTO) Some(instruction.asInstanceOf[JumpInsnNode]) + else None + } + } + + object JumpNonJsr { + def unapply(instruction: AbstractInsnNode): Option[JumpInsnNode] = { + if (isJumpNonJsr(instruction)) Some(instruction.asInstanceOf[JumpInsnNode]) + else None + } + } + + object ConditionalJump { + def unapply(instruction: AbstractInsnNode): Option[JumpInsnNode] = { + if (isConditionalJump(instruction)) Some(instruction.asInstanceOf[JumpInsnNode]) + else None + } + } + + object VarInstruction { + def unapply(instruction: AbstractInsnNode): Option[VarInsnNode] = { + if (isVarInstruction(instruction)) Some(instruction.asInstanceOf[VarInsnNode]) + else None + } + + } + + 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) + } + + def isConditionalJump(instruction: AbstractInsnNode): Boolean = { + val op = instruction.getOpcode + (op >= Opcodes.IFEQ && op <= Opcodes.IF_ACMPNE) || op == Opcodes.IFNULL || op == Opcodes.IFNONNULL + } + + def isReturn(instruction: AbstractInsnNode): Boolean = { + val op = instruction.getOpcode + op >= Opcodes.IRETURN && op <= Opcodes.RETURN + } + + def isVarInstruction(instruction: AbstractInsnNode): Boolean = { + val op = instruction.getOpcode + (op >= Opcodes.ILOAD && op <= Opcodes.ALOAD) || (op >= Opcodes.ISTORE && op <= Opcodes.ASTORE) + } + + def isExecutable(instruction: AbstractInsnNode): Boolean = instruction.getOpcode >= 0 + + 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) + } + + def sameTargetExecutableInstruction(a: JumpInsnNode, b: JumpInsnNode): Boolean = { + // Compare next executable instead of the the labels. Identifies a, b as the same target: + // LabelNode(a) + // LabelNode(b) + // Instr + nextExecutableInstruction(a.label) == nextExecutableInstruction(b.label) + } + + 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) { + instructions.insert(jump, getPop(1)) + } else if ((op >= Opcodes.IF_ICMPEQ && op <= Opcodes.IF_ICMPLE) || op == Opcodes.IF_ACMPEQ || op == Opcodes.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) + } + instructions.remove(jump) + } + + def finalJumpTarget(source: JumpInsnNode): LabelNode = { + @tailrec def followGoto(label: LabelNode, seenLabels: Set[LabelNode]): LabelNode = nextExecutableInstruction(label) match { + case Some(Goto(dest)) => + if (seenLabels(dest.label)) dest.label + else followGoto(dest.label, seenLabels + dest.label) + + case _ => label + } + followGoto(source.label, Set(source.label)) + } + + def negateJumpOpcode(jumpOpcode: Int): Int = (jumpOpcode: @switch) match { + case Opcodes.IFEQ => Opcodes.IFNE + case Opcodes.IFNE => Opcodes.IFEQ + + case Opcodes.IFLT => Opcodes.IFGE + case Opcodes.IFGE => Opcodes.IFLT + + case Opcodes.IFGT => Opcodes.IFLE + case Opcodes.IFLE => Opcodes.IFGT + + case Opcodes.IF_ICMPEQ => Opcodes.IF_ICMPNE + case Opcodes.IF_ICMPNE => Opcodes.IF_ICMPEQ + + case Opcodes.IF_ICMPLT => Opcodes.IF_ICMPGE + case Opcodes.IF_ICMPGE => Opcodes.IF_ICMPLT + + case Opcodes.IF_ICMPGT => Opcodes.IF_ICMPLE + case Opcodes.IF_ICMPLE => Opcodes.IF_ICMPGT + + case Opcodes.IF_ACMPEQ => Opcodes.IF_ACMPNE + case Opcodes.IF_ACMPNE => Opcodes.IF_ACMPEQ + + case Opcodes.IFNULL => Opcodes.IFNONNULL + case Opcodes.IFNONNULL => Opcodes.IFNULL + } + + def getPop(size: Int): InsnNode = { + val op = if (size == 1) Opcodes.POP else Opcodes.POP2 + new InsnNode(op) + } + + 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) + + method.instructions.iterator().asScala foreach { + case jump: JumpInsnNode => add(jump.label, jump) + case line: LineNumberNode => add(line.start, line) + case switch: LookupSwitchInsnNode => switch.labels.asScala.foreach(add(_, switch)); add(switch.dflt, switch) + case switch: TableSwitchInsnNode => switch.labels.asScala.foreach(add(_, switch)); add(switch.dflt, switch) + case _ => + } + if (method.localVariables != null) { + method.localVariables.iterator().asScala.foreach(l => { add(l.start, l); add(l.end, l) }) + } + if (method.tryCatchBlocks != null) { + method.tryCatchBlocks.iterator().asScala.foreach(l => { add(l.start, l); add(l.handler, l); add(l.end, l) }) + } + + res.toMap + } + + def substituteLabel(reference: AnyRef, from: LabelNode, to: LabelNode): Unit = { + def substList(list: java.util.List[LabelNode]) = { + foreachWithIndex(list.asScala.toList) { case (l, i) => + if (l == from) list.set(i, to) + } + } + reference match { + case jump: JumpInsnNode => jump.label = to + case line: LineNumberNode => line.start = to + case switch: LookupSwitchInsnNode => substList(switch.labels); if (switch.dflt == from) switch.dflt = to + case switch: TableSwitchInsnNode => substList(switch.labels); if (switch.dflt == from) switch.dflt = to + case local: LocalVariableNode => + if (local.start == from) local.start = to + if (local.end == from) local.end = to + case handler: TryCatchBlockNode => + if (handler.start == from) handler.start = to + if (handler.handler == from) handler.handler = to + if (handler.end == from) handler.end = to + } + } +} diff --git a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala new file mode 100644 index 000000000000..273112b93c86 --- /dev/null +++ b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala @@ -0,0 +1,562 @@ +/* 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, MethodWriter, ClassWriter} +import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter} +import scala.tools.asm.tree._ +import scala.collection.convert.decorateAsScala._ +import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ +import scala.tools.nsc.settings.ScalaSettings + +/** + * Optimizations within a single method. + * + * unreachable code + * - removes instrucions 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 + * unreachble. in this case "unreachable code" is invoked recursively until reaching a fixpiont. + * > for try blocks that are unreachable, "unreachable code" removes also the instructions of the + * catch block, and the recrusive invocation is not necessary. + * + * simplify jumps + * - various simplifications, see doc domments 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(settings: ScalaSettings) { + /** + * 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 elminated in some method, `false` otherwise. + */ + def methodOptimizations(clazz: ClassNode): Boolean = { + settings.Yopt.value.nonEmpty && 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 analyis 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. + */ + private def methodOptimizations(method: MethodNode, ownerClassName: String): 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. + + var recurse = true + var codeHandlersOrJumpsChanged = false + while (recurse) { + // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) + val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (settings.YoptUnreachableCode) { + val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) + val removedHandlers = removeEmptyExceptionHandlers(method) + (codeRemoved, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) + } else { + (false, false, false) + } + + val jumpsChanged = if (settings.YoptSimplifyJumps) simplifyJumps(method) else false + + codeHandlersOrJumpsChanged ||= (codeRemoved || handlersRemoved || jumpsChanged) + + // The doc comment of class LocalOpt explains why we recurse if jumpsChanged || liveHandlerRemoved + recurse = settings.YoptRecurseUnreachableJumps && (jumpsChanged || liveHandlerRemoved) + } + + // (*) Removing stale local variable descriptors is required for correctness of unreachable-code + val localsRemoved = + if (settings.YoptCompactLocals) compactLocalVariables(method) + else if (settings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*) + else false + + val lineNumbersRemoved = if (settings.YoptEmptyLineNumbers) removeEmptyLineNumbers(method) else false + + val labelsRemoved = if (settings.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) + + codeHandlersOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + } + + /** + * Removes unreachable basic blocks. + * + * TODO: rewrite, don't use computeMaxLocalsMaxStack (runs a ClassWriter) / Analyzer. Too slow. + */ + def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: String): (Boolean, Set[LabelNode]) = { + // The data flow analysis requires the maxLocals / maxStack fields of the method to be computed. + computeMaxLocalsMaxStack(method) + val a = new Analyzer[BasicValue](new BasicInterpreter) + a.analyze(ownerClassName, method) + val frames = a.getFrames + + val initialSize = method.instructions.size + var i = 0 + var liveLabels = Set.empty[LabelNode] + 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() + } + } + i += 1 + } + (method.instructions.size != initialSize, 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)(fistLocalIndex: 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 >= fistLocalIndex) { + 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 varialbe 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 + } + } + + /** + * In order to run an Analyzer, the maxLocals / maxStack fields need to be available. The ASM + * framework only computes these values during bytecode generation. + * + * Sicne there's currently no better way, we run a bytecode generator on the method and extract + * the computed values. This required changes to the ASM codebase: + * - the [[MethodWriter]] class was made public + * - accessors for maxLocals / maxStack were added to the MethodWriter class + * + * We could probably make this faster (and allocate less memory) by hacking the ASM framework + * more: create a subclass of MethodWriter with a /dev/null byteVector. Another option would be + * to create a separate visitor for computing those values, duplicating the functionality from the + * MethodWriter. + */ + private def computeMaxLocalsMaxStack(method: MethodNode) { + val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) + val excs = method.exceptions.asScala.toArray + val mw = cw.visitMethod(method.access, method.name, method.desc, method.signature, excs).asInstanceOf[MethodWriter] + method.accept(mw) + method.maxLocals = mw.getMaxLocals + method.maxStack = mw.getMaxStack + } + + /** + * Removes LineNumberNodes that don't describe any executable instructions. + * + * This method expects (and asserts) that the `start` label of each LineNumberNode is the + * lexically preceeding 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 unchaned] + * + * 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. Otherwsie, 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 insructions 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 + }) +} From 70580ffb29bd7dea88fcecf3e242c53d60f47a6e Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Tue, 31 Oct 2017 11:03:14 +0100 Subject: [PATCH 2/5] Fix compilation of backend/LocalOpt.scala ...by removing all the settings and having them at true by default. --- .../tools/backend/jvm/opt/BytecodeUtils.scala | 6 ++--- .../tools/backend/jvm/opt/LocalOpt.scala | 23 ++++++++++--------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala b/compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala index 6b4047c0a786..eb5597c02071 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/BytecodeUtils.scala @@ -9,7 +9,7 @@ package opt import scala.annotation.{tailrec, switch} import scala.collection.mutable -import scala.reflect.internal.util.Collections._ +// import scala.reflect.internal.util.Collections._ import scala.tools.asm.Opcodes import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ @@ -83,7 +83,7 @@ object BytecodeUtils { nextExecutableInstruction(a.label) == nextExecutableInstruction(b.label) } - def removeJumpAndAdjustStack(method: MethodNode, jump: JumpInsnNode) { + def removeJumpAndAdjustStack(method: MethodNode, jump: JumpInsnNode): Unit = { val instructions = method.instructions val op = jump.getOpcode if ((op >= Opcodes.IFEQ && op <= Opcodes.IFGE) || op == Opcodes.IFNULL || op == Opcodes.IFNONNULL) { @@ -163,7 +163,7 @@ object BytecodeUtils { def substituteLabel(reference: AnyRef, from: LabelNode, to: LabelNode): Unit = { def substList(list: java.util.List[LabelNode]) = { - foreachWithIndex(list.asScala.toList) { case (l, i) => + list.asScala.toList.zipWithIndex.foreach { case (l, i) => if (l == from) list.set(i, to) } } diff --git a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala index 273112b93c86..f9bd58899226 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala @@ -13,7 +13,7 @@ import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter} import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ -import scala.tools.nsc.settings.ScalaSettings +// import scala.tools.nsc.settings.ScalaSettings /** * Optimizations within a single method. @@ -46,7 +46,7 @@ import scala.tools.nsc.settings.ScalaSettings * stale labels * - eliminate labels that are not referenced, merge sequences of label definitions. */ -class LocalOpt(settings: ScalaSettings) { +class LocalOpt(/*settings: ScalaSettings*/) { /** * Remove unreachable instructions from all (non-abstract) methods and apply various other * cleanups to the bytecode. @@ -55,7 +55,8 @@ class LocalOpt(settings: ScalaSettings) { * @return `true` if unreachable code was elminated in some method, `false` otherwise. */ def methodOptimizations(clazz: ClassNode): Boolean = { - settings.Yopt.value.nonEmpty && clazz.methods.asScala.foldLeft(false) { + // settings.Yopt.value.nonEmpty + true && clazz.methods.asScala.foldLeft(false) { case (changed, method) => methodOptimizations(method, clazz.name) || changed } } @@ -106,7 +107,7 @@ class LocalOpt(settings: ScalaSettings) { var codeHandlersOrJumpsChanged = false while (recurse) { // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) - val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (settings.YoptUnreachableCode) { + val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (true /*settings.YoptUnreachableCode*/) { val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) val removedHandlers = removeEmptyExceptionHandlers(method) (codeRemoved, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) @@ -114,23 +115,23 @@ class LocalOpt(settings: ScalaSettings) { (false, false, false) } - val jumpsChanged = if (settings.YoptSimplifyJumps) simplifyJumps(method) else false + val jumpsChanged = if (true /*settings.YoptSimplifyJumps*/) simplifyJumps(method) else false codeHandlersOrJumpsChanged ||= (codeRemoved || handlersRemoved || jumpsChanged) // The doc comment of class LocalOpt explains why we recurse if jumpsChanged || liveHandlerRemoved - recurse = settings.YoptRecurseUnreachableJumps && (jumpsChanged || liveHandlerRemoved) + recurse = /*settings.YoptRecurseUnreachableJumps &&*/ (jumpsChanged || liveHandlerRemoved) } // (*) Removing stale local variable descriptors is required for correctness of unreachable-code val localsRemoved = - if (settings.YoptCompactLocals) compactLocalVariables(method) - else if (settings.YoptUnreachableCode) removeUnusedLocalVariableNodes(method)() // (*) + if (true /*settings.YoptCompactLocals*/) compactLocalVariables(method) + else if (true /*settings.YoptUnreachableCode*/) removeUnusedLocalVariableNodes(method)() // (*) else false - val lineNumbersRemoved = if (settings.YoptEmptyLineNumbers) removeEmptyLineNumbers(method) else false + val lineNumbersRemoved = if (true /*settings.YoptEmptyLineNumbers*/) removeEmptyLineNumbers(method) else false - val labelsRemoved = if (settings.YoptEmptyLabels) removeEmptyLabelNodes(method) else false + val labelsRemoved = if (true /*settings.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. @@ -332,7 +333,7 @@ class LocalOpt(settings: ScalaSettings) { * to create a separate visitor for computing those values, duplicating the functionality from the * MethodWriter. */ - private def computeMaxLocalsMaxStack(method: MethodNode) { + private def computeMaxLocalsMaxStack(method: MethodNode): Unit = { val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS) val excs = method.exceptions.asScala.toArray val mw = cw.visitMethod(method.access, method.name, method.desc, method.signature, excs).asInstanceOf[MethodWriter] From 6831a49256b406393c0bce852c37742ce7cafc03 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Tue, 31 Oct 2017 11:04:58 +0100 Subject: [PATCH 3/5] Unable backend/LocalOpt.scala --- compiler/src/dotty/tools/backend/jvm/GenBCode.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala index ad84bde680d1..06feccabb548 100644 --- a/compiler/src/dotty/tools/backend/jvm/GenBCode.scala +++ b/compiler/src/dotty/tools/backend/jvm/GenBCode.scala @@ -248,10 +248,10 @@ class GenBCodePipeline(val entryPoints: List[Symbol], val int: DottyBackendInter * - converting the plain ClassNode to byte array and placing it on queue-3 */ class Worker2 { - // lazy val localOpt = new LocalOpt(new Settings()) + lazy val localOpt = new opt.LocalOpt(/*new Settings()*/) def localOptimizations(classNode: ClassNode): Unit = { - // BackendStats.timed(BackendStats.methodOptTimer)(localOpt.methodOptimizations(classNode)) + /*BackendStats.timed(BackendStats.methodOptTimer)*/(localOpt.methodOptimizations(classNode)) } def run(): Unit = { From eee7b6b322e6f889a17c4f3f1e9916c115c47fb5 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Tue, 31 Oct 2017 11:34:15 +0100 Subject: [PATCH 4/5] Put backend optimizations behind a flag And use then when bootstraping. With this change the CI benchmarks are going to use a optimized bootstraped compiler but will continue doing mesurement without these optimizations. This way we can measure the speedup of these optimization on the compiler without measuring the cost of doing the optimizations. --- .../tools/backend/jvm/opt/LocalOpt.scala | 20 ++++++++++--------- .../tools/dotc/config/ScalaSettings.scala | 2 +- project/Build.scala | 2 ++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala index f9bd58899226..c7132000b606 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala @@ -13,6 +13,8 @@ import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter} import scala.tools.asm.tree._ import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ +import dotty.tools.dotc.core.Contexts.Context + // import scala.tools.nsc.settings.ScalaSettings /** @@ -46,7 +48,7 @@ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ * stale labels * - eliminate labels that are not referenced, merge sequences of label definitions. */ -class LocalOpt(/*settings: ScalaSettings*/) { +class LocalOpt(implicit ctx: Context) { /** * Remove unreachable instructions from all (non-abstract) methods and apply various other * cleanups to the bytecode. @@ -56,7 +58,7 @@ class LocalOpt(/*settings: ScalaSettings*/) { */ def methodOptimizations(clazz: ClassNode): Boolean = { // settings.Yopt.value.nonEmpty - true && clazz.methods.asScala.foldLeft(false) { + ctx.settings.YbackendOpt.value && clazz.methods.asScala.foldLeft(false) { case (changed, method) => methodOptimizations(method, clazz.name) || changed } } @@ -107,7 +109,7 @@ class LocalOpt(/*settings: ScalaSettings*/) { var codeHandlersOrJumpsChanged = false while (recurse) { // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) - val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (true /*settings.YoptUnreachableCode*/) { + val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (ctx.settings.YbackendOpt.value) { val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) val removedHandlers = removeEmptyExceptionHandlers(method) (codeRemoved, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) @@ -115,23 +117,23 @@ class LocalOpt(/*settings: ScalaSettings*/) { (false, false, false) } - val jumpsChanged = if (true /*settings.YoptSimplifyJumps*/) simplifyJumps(method) else false + val jumpsChanged = if (ctx.settings.YbackendOpt.value) simplifyJumps(method) else false codeHandlersOrJumpsChanged ||= (codeRemoved || handlersRemoved || jumpsChanged) // The doc comment of class LocalOpt explains why we recurse if jumpsChanged || liveHandlerRemoved - recurse = /*settings.YoptRecurseUnreachableJumps &&*/ (jumpsChanged || liveHandlerRemoved) + recurse = ctx.settings.YbackendOpt.value && (jumpsChanged || liveHandlerRemoved) } // (*) Removing stale local variable descriptors is required for correctness of unreachable-code val localsRemoved = - if (true /*settings.YoptCompactLocals*/) compactLocalVariables(method) - else if (true /*settings.YoptUnreachableCode*/) removeUnusedLocalVariableNodes(method)() // (*) + if (ctx.settings.YbackendOpt.value) compactLocalVariables(method) + else if (ctx.settings.YbackendOpt.value) removeUnusedLocalVariableNodes(method)() // (*) else false - val lineNumbersRemoved = if (true /*settings.YoptEmptyLineNumbers*/) removeEmptyLineNumbers(method) else false + val lineNumbersRemoved = if (ctx.settings.YbackendOpt.value) removeEmptyLineNumbers(method) else false - val labelsRemoved = if (true /*settings.YoptEmptyLabels*/) removeEmptyLabelNodes(method) else false + val labelsRemoved = if (ctx.settings.YbackendOpt.value) 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/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala index f2b854f41fb1..cc596bbff67f 100644 --- a/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala +++ b/compiler/src/dotty/tools/dotc/config/ScalaSettings.scala @@ -110,7 +110,7 @@ class ScalaSettings extends Settings.SettingGroup { val YshowVarBounds = BooleanSetting("-Yshow-var-bounds", "Print type variables with their bounds") val YnoInline = BooleanSetting("-Yno-inline", "Suppress inlining.") - /** Linker specific flags */ + val YbackendOpt = BooleanSetting("-Ybackend-opt", "Enable backend optimisations") val YoptPhases = PhasesSetting("-Yopt-phases", "Restrict the optimisation phases to execute under -optimise.") val YoptFuel = IntSetting("-Yopt-fuel", "Maximum number of optimisations performed under -optimise.", -1) val optimise = BooleanSetting("-optimise", "Generates faster bytecode by applying local optimisations to the .program") withAbbreviation "-optimize" diff --git a/project/Build.scala b/project/Build.scala index d3ee4f0d01a0..bfac30729fe9 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -177,6 +177,8 @@ object Build { // otherwise sbt 0.13 incremental compilation breaks (https://github.com/sbt/sbt/issues/3142) scalacOptions ++= Seq("-bootclasspath", sys.props("sun.boot.class.path")), + scalacOptions += "-Ybackend-opt", + // sbt gets very unhappy if two projects use the same target target := baseDirectory.value / ".." / "out" / "bootstrap" / name.value, From 6a7941567e3249e2edc2ec522c46df6a18bc9fa6 Mon Sep 17 00:00:00 2001 From: Olivier Blanvillain Date: Mon, 6 Nov 2017 09:56:53 +0100 Subject: [PATCH 5/5] Always enable backend optimizations (To mesure performence and the CI) --- .../dotty/tools/backend/jvm/opt/LocalOpt.scala | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala index c7132000b606..fa443f4caeee 100644 --- a/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala +++ b/compiler/src/dotty/tools/backend/jvm/opt/LocalOpt.scala @@ -58,7 +58,7 @@ class LocalOpt(implicit ctx: Context) { */ def methodOptimizations(clazz: ClassNode): Boolean = { // settings.Yopt.value.nonEmpty - ctx.settings.YbackendOpt.value && clazz.methods.asScala.foldLeft(false) { + true && clazz.methods.asScala.foldLeft(false) { case (changed, method) => methodOptimizations(method, clazz.name) || changed } } @@ -109,7 +109,7 @@ class LocalOpt(implicit ctx: Context) { var codeHandlersOrJumpsChanged = false while (recurse) { // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) - val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (ctx.settings.YbackendOpt.value) { + val (codeRemoved, handlersRemoved, liveHandlerRemoved) = if (true) { val (codeRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) val removedHandlers = removeEmptyExceptionHandlers(method) (codeRemoved, removedHandlers.nonEmpty, removedHandlers.exists(h => liveLabels(h.start))) @@ -117,23 +117,23 @@ class LocalOpt(implicit ctx: Context) { (false, false, false) } - val jumpsChanged = if (ctx.settings.YbackendOpt.value) simplifyJumps(method) else false + val jumpsChanged = if (true) simplifyJumps(method) else false codeHandlersOrJumpsChanged ||= (codeRemoved || handlersRemoved || jumpsChanged) // The doc comment of class LocalOpt explains why we recurse if jumpsChanged || liveHandlerRemoved - recurse = ctx.settings.YbackendOpt.value && (jumpsChanged || liveHandlerRemoved) + recurse = true && (jumpsChanged || liveHandlerRemoved) } // (*) Removing stale local variable descriptors is required for correctness of unreachable-code val localsRemoved = - if (ctx.settings.YbackendOpt.value) compactLocalVariables(method) - else if (ctx.settings.YbackendOpt.value) removeUnusedLocalVariableNodes(method)() // (*) + if (true) compactLocalVariables(method) + else if (true) removeUnusedLocalVariableNodes(method)() // (*) else false - val lineNumbersRemoved = if (ctx.settings.YbackendOpt.value) removeEmptyLineNumbers(method) else false + val lineNumbersRemoved = if (true) removeEmptyLineNumbers(method) else false - val labelsRemoved = if (ctx.settings.YbackendOpt.value) removeEmptyLabelNodes(method) else false + val labelsRemoved = if (true) 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.