diff options
Diffstat (limited to 'src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala')
-rw-r--r-- | src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala | 856 |
1 files changed, 608 insertions, 248 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala index 4132710a96..f120357c63 100644 --- a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala +++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala @@ -7,79 +7,180 @@ 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.annotation.{tailrec, switch} + +import scala.tools.asm.Type +import scala.tools.asm.tree.analysis.Frame +import scala.tools.asm.Opcodes._ import scala.tools.asm.tree._ +import scala.collection.mutable import scala.collection.convert.decorateAsScala._ import scala.tools.nsc.backend.jvm.BTypes.InternalName +import scala.tools.nsc.backend.jvm.analysis._ import scala.tools.nsc.backend.jvm.opt.BytecodeUtils._ /** - * Optimizations within a single method. + * Optimizations within a single method. Certain optimizations enable others, for example removing + * unreachable code can render a `try` block empty and enable removeEmptyExceptionHandlers. The + * latter in turn enables more unreachable code to be eliminated (the `catch` block), so there is + * a cyclic dependency. Optimizations that depend on each other are therefore executed in a loop + * until reaching a fixpoint. + * + * The optimizations marked UPSTREAM enable optimizations that were already executed, so they cause + * another iteration in the fixpoint loop. + * + * nullness optimizations: rewrite null-checking branches to GOTO if nullness is known + * + enables downstream + * - unreachable code (null / non-null branch becomes unreachable) + * - box-unbox elimination (may render an escaping consumer of a box unreachable) + * - stale stores (aload x is replaced by aconst_null if it's known null) + * - simplify jumps (replaces conditional jumps by goto, so may enable goto chains) + * + * unreachable code / DCE (removes instructions of basic blocks to which there is no branch) + * + enables downstream: + * - stale stores (loads may be eliminated, removing consumers of a store) + * - 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: + * - nullness optimizations (a box extraction operation (unknown nullness) may be rewritten to + * a read of a non-null local. example in doc comment of box-unbox implementation) + * - further box-unbox elimination (e.g. an Integer stored in a Tuple; eliminating the tuple may + * enable eliminating the Integer) + * + enables downstream: + * - copy propagation (new locals are introduced, may be aliases of existing) + * - stale stores (multi-value boxes where not all values are used) + * - redundant casts (`("a", "b")._1`: the generic `_1` method returns `Object`, a cast + * to String is added. The cast is redundant after eliminating the tuple.) + * - empty local variable descriptors (local variables that were holding the box may become unused) + * + * copy propagation (replaces LOAD n to the LOAD m for the smallest m that is an alias of n) + * + enables downstrem: + * - stale stores (a stored value may not be loaded anymore) + * - store-load pairs (a load n may now be right after a store n) + * + NOTE: copy propagation is only executed once, in the first fixpoint loop iteration. none of + * the other optimizations enables further copy prop. we still run it as part of the loop + * because it requires unreachable code to be eliminated. + * + * stale stores (replace STORE by POP) + * + enables downstream: + * - push-pop (the new pop may be the single consumer for an instruction) + * + * redundant casts: eliminates casts that are statically known to succeed (uses type propagation) + * + enables UPSTREAM: + * - box-unbox elimination (a removed checkcast may be a box consumer) + * + enables downstream: + * - push-pop for closure allocation elimination (every indyLambda is followed by a checkcast, see SI-9540) + * + * push-pop (when a POP is the only consumer of a value, remove the POP and its producer) + * + enables UPSTREAM: + * - stale stores (if a LOAD is removed, a corresponding STORE may become stale) + * - 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) + * - simplify jumps (push-pop removes code) + * + * store-load pairs (remove `STORE x; LOAD x` if x is otherwise not used in the method) + * + enables downstream: + * - empty handlers (code is removes, a try block may become empty + * - simplify jumps (code is removed, a goto may become redundant for example) + * - stale local variable descriptors * - * 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 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 * - * 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, like `GOTO l; l: ...`, see doc comments of individual optimizations) + * + enables UPSTREAM + * - unreachable code (`GOTO a; a: GOTO b; b: ...`, the first jump is changed to `GOTO b`, the second becomes unreachable) + * - store-load pairs (a `GOTO l; l: ...` is removed between store and load) + * - push-pop (`IFNULL l; l: ...` is replaced by `POP`) * - * 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) + * The following cleanup optimizations don't enable any upstream optimizations, so they can be + * executed once at the end, when the above optimizations reach a fixpoint. * - * empty 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. + * empty local variable descriptors (removes unused variables from the local variable table) + * + enables downstream: + * - stale labels (labels that the entry points to, if not otherwise referenced) + * + * empty line numbers (eliminates line number nodes that describe no executable instructions) + * + enables downstream: + * - stale labels (label of the line number node, if not otherwise referenced) + * + * stale labels (eliminate labels that are not referenced, merge sequences of label definitions) + * + * + * Note on a method's maxLocals / maxStack: the backend only uses those values for running + * Analyzers. The values can be conservative approximations: if an optimization removes code and + * the maximal stack size is now smaller, the larger maxStack value will still work fine for + * running an Analyzer (just that frames allocate more space than required). The correct max + * values written to the bytecode are re-computed during classfile serialization. + * To keep things simpler, we don't update the max values in every optimization: + * - we do it in `removeUnreachableCodeImpl`, because it's quite straightforward + * - maxLocals is updated in `compactLocalVariables`, which runs at the end of method optimizations + * + * + * Note on updating the call graph: whenever an optimization eliminates a callsite or a closure + * instantiation, we eliminate the corresponding entry from the call graph. */ class LocalOpt[BT <: BTypes](val btypes: BT) { import LocalOptImpls._ import btypes._ + import coreBTypes._ + import backendUtils._ + + val boxUnbox = new BoxUnbox(btypes) + import boxUnbox._ + + val copyProp = new CopyProp(btypes) + import copyProp._ /** * 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. + * depends 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 + def minimalRemoveUnreachableCode(method: MethodNode, ownerClassName: InternalName): Boolean = { + // In principle, for the inliner, a single removeUnreachableCodeImpl would be enough. But that + // would potentially leave behind stale handlers (empty try block) which is not legal in the + // classfile. So we run both removeUnreachableCodeImpl and removeEmptyExceptionHandlers. + if (method.instructions.size == 0) return false // fast path for abstract methods + if (unreachableCodeEliminated(method)) return false // we know there is no unreachable code + if (!AsmAnalyzer.sizeOKForBasicValue(method)) return false // the method is too large for running an analyzer // For correctness, after removing unreachable code, we have to eliminate empty exception // handlers, see scaladoc of def methodOptimizations. Removing an live handler may render more // code unreachable and therefore requires running another round. - def removalRound(): Set[AbstractInsnNode] = { - val (removedInstructions, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) - val removedRecursively = if (removedInstructions.nonEmpty) { + def removalRound(): Boolean = { + val (insnsRemoved, liveLabels) = removeUnreachableCodeImpl(method, ownerClassName) + if (insnsRemoved) { val liveHandlerRemoved = removeEmptyExceptionHandlers(method).exists(h => liveLabels(h.start)) if (liveHandlerRemoved) removalRound() - else Set.empty - } else Set.empty - removedInstructions ++ removedRecursively + } + insnsRemoved } - val removedInstructions = removalRound() - if (removedInstructions.nonEmpty) removeUnusedLocalVariableNodes(method)() + val changed = removalRound() + if (changed) removeUnusedLocalVariableNodes(method)() unreachableCodeEliminated += method - removedInstructions + changed } /** @@ -96,15 +197,7 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { } /** - * Remove unreachable code from a method. - * - * We rely on dead code elimination provided by the ASM framework, as described in the ASM User - * Guide (http://asm.ow2.org/index.html), Section 8.2.1. It runs a data flow analysis, which only - * computes Frame information for reachable instructions. Instructions for which no Frame data is - * available after the analysis are unreachable. - * - * Also simplifies branching instructions, removes unused local variable descriptors, empty - * exception handlers, unnecessary label declarations and empty line number nodes. + * Run method-level optimizations, see comment on class [[LocalOpt]]. * * Returns `true` if the bytecode of `method` was changed. */ @@ -137,36 +230,150 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { // This triggers "ClassFormatError: Illegal exception table range in class file C". Similar // for local variables in dead blocks. Maybe that's a bug in the ASM framework. - def removalRound(): Boolean = { - // unreachable-code, empty-handlers and simplify-jumps run until reaching a fixpoint (see doc on class LocalOpt) - val (codeRemoved, handlersRemoved, liveHandlerRemoved) = 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) + var currentTrace: String = null + val doTrace = compilerSettings.YoptTrace.isSetByUser && compilerSettings.YoptTrace.value == ownerClassName + "." + method.name + def traceIfChanged(optName: String): Unit = if (doTrace) { + val after = AsmUtils.textify(method) + if (currentTrace != after) { + println(s"after $optName") + println(after) } - - 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 + currentTrace = after } - val codeHandlersOrJumpsChanged = removalRound() + /** + * 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( + requestNullness: Boolean, + requestDCE: Boolean, + requestBoxUnbox: Boolean, + requestStaleStores: Boolean, + requestPushPop: Boolean, + requestStoreLoad: Boolean, + firstIteration: Boolean, + maxRecursion: Int = 10): (Boolean, Boolean) = { + if (maxRecursion == 0) return (false, false) + + traceIfChanged("beforeMethodOpt") + + // NULLNESS OPTIMIZATIONS + val runNullness = compilerSettings.YoptNullnessTracking && requestNullness + val nullnessOptChanged = runNullness && nullnessOptimizations(method, ownerClassName) + traceIfChanged("nullness") + + // UNREACHABLE CODE + // Both AliasingAnalyzer (used in copyProp) and ProdConsAnalyzer (used in eliminateStaleStores, + // boxUnboxElimination) require not having unreachable instructions (null frames). + val runDCE = (compilerSettings.YoptUnreachableCode && (requestDCE || nullnessOptChanged)) || + compilerSettings.YoptBoxUnbox || + compilerSettings.YoptCopyPropagation + val (codeRemoved, liveLabels) = if (runDCE) removeUnreachableCodeImpl(method, ownerClassName) else (false, Set.empty[LabelNode]) + traceIfChanged("dce") + + // BOX-UNBOX + val runBoxUnbox = compilerSettings.YoptBoxUnbox && (requestBoxUnbox || nullnessOptChanged) + val boxUnboxChanged = runBoxUnbox && boxUnboxElimination(method, ownerClassName) + traceIfChanged("boxUnbox") + + // COPY PROPAGATION + val runCopyProp = compilerSettings.YoptCopyPropagation && (firstIteration || boxUnboxChanged) + val copyPropChanged = runCopyProp && copyPropagation(method, ownerClassName) + traceIfChanged("copyProp") + + // STALE STORES + val runStaleStores = compilerSettings.YoptCopyPropagation && (requestStaleStores || nullnessOptChanged || codeRemoved || boxUnboxChanged || copyPropChanged) + val storesRemoved = runStaleStores && eliminateStaleStores(method, ownerClassName) + traceIfChanged("staleStores") + + // REDUNDANT CASTS + val runRedundantCasts = compilerSettings.YoptRedundantCasts && (firstIteration || boxUnboxChanged) + val castRemoved = runRedundantCasts && eliminateRedundantCasts(method, ownerClassName) + traceIfChanged("redundantCasts") + + // PUSH-POP + val runPushPop = compilerSettings.YoptCopyPropagation && (requestPushPop || firstIteration || storesRemoved || castRemoved) + val pushPopRemoved = runPushPop && eliminatePushPop(method, ownerClassName) + traceIfChanged("pushPop") + + // STORE-LOAD PAIRS + val runStoreLoad = compilerSettings.YoptCopyPropagation && (requestStoreLoad || boxUnboxChanged || copyPropChanged || pushPopRemoved) + val storeLoadRemoved = runStoreLoad && eliminateStoreLoad(method) + traceIfChanged("storeLoadPairs") + + // STALE HANDLERS + val removedHandlers = if (runDCE) removeEmptyExceptionHandlers(method) else Set.empty[TryCatchBlockNode] + val handlersRemoved = removedHandlers.nonEmpty + val liveHandlerRemoved = removedHandlers.exists(h => liveLabels(h.start)) + traceIfChanged("staleHandlers") + + // SIMPLIFY JUMPS + // almost all of the above optimizations enable simplifying more jumps, so we just run it in every iteration + val runSimplifyJumps = compilerSettings.YoptSimplifyJumps + val jumpsChanged = runSimplifyJumps && simplifyJumps(method) + traceIfChanged("simplifyJumps") + + // See doc comment in the beginning of this file (optimizations marked UPSTREAM) + val runNullnessAgain = boxUnboxChanged + val runDCEAgain = liveHandlerRemoved || jumpsChanged + val runBoxUnboxAgain = boxUnboxChanged || castRemoved || pushPopRemoved || liveHandlerRemoved + val runStaleStoresAgain = pushPopRemoved + val runPushPopAgain = jumpsChanged + val runStoreLoadAgain = jumpsChanged + val runAgain = runNullnessAgain || runDCEAgain || runBoxUnboxAgain || pushPopRemoved || runStaleStoresAgain || runPushPopAgain || runStoreLoadAgain + + val downstreamRequireEliminateUnusedLocals = runAgain && removalRound( + requestNullness = runNullnessAgain, + requestDCE = runDCEAgain, + requestBoxUnbox = runBoxUnboxAgain, + requestStaleStores = runStaleStoresAgain, + requestPushPop = runPushPopAgain, + requestStoreLoad = runStoreLoadAgain, + firstIteration = false, + maxRecursion = maxRecursion - 1)._2 + + val requireEliminateUnusedLocals = downstreamRequireEliminateUnusedLocals || + nullnessOptChanged || // nullness opt may eliminate stores / loads, rendering a local unused + codeRemoved || // see comment in method `methodOptimizations` + boxUnboxChanged || // box-unbox renders locals (holding boxes) unused + storesRemoved || + storeLoadRemoved || + handlersRemoved + + val codeChanged = nullnessOptChanged || codeRemoved || boxUnboxChanged || castRemoved || copyPropChanged || storesRemoved || pushPopRemoved || storeLoadRemoved || handlersRemoved || jumpsChanged + (codeChanged, requireEliminateUnusedLocals) + } - // (*) Removing stale local variable descriptors is required for correctness of unreachable-code + val (nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged, requireEliminateUnusedLocals) = if (AsmAnalyzer.sizeOKForBasicValue(method)) { + // we run DCE even if the method is already in the `unreachableCodeEliminated` map: the DCE + // here is more thorough than `minimalRemoveUnreachableCode` that run before inlining. + val r = removalRound( + requestNullness = true, + requestDCE = true, + requestBoxUnbox = true, + requestStaleStores = true, + requestPushPop = true, + requestStoreLoad = true, + firstIteration = true) + if (compilerSettings.YoptUnreachableCode) unreachableCodeEliminated += method + r + } else (false, false) + + // (*) 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 + traceIfChanged("localVariables") - val lineNumbersRemoved = if (compilerSettings.YoptEmptyLineNumbers) removeEmptyLineNumbers(method) else false + val lineNumbersRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLineNumbers(method) else false + traceIfChanged("lineNumbers") - val labelsRemoved = if (compilerSettings.YoptEmptyLabels) removeEmptyLabelNodes(method) else false + val labelsRemoved = if (compilerSettings.YoptUnreachableCode) removeEmptyLabelNodes(method) else false + traceIfChanged("labels") // assert that local variable annotations are empty (we don't emit them) - otherwise we'd have // to eliminate those covering an empty range, similar to removeUnusedLocalVariableNodes. @@ -174,53 +381,198 @@ class LocalOpt[BT <: BTypes](val btypes: BT) { assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations) assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations) - unreachableCodeEliminated += method - - codeHandlersOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved + nullnessDceBoxesCastsCopypropPushpopOrJumpsChanged || localsRemoved || lineNumbersRemoved || labelsRemoved } -} + /** + * Apply various optimizations based on nullness analysis information. + * - IFNULL / IFNONNULL are rewritten to GOTO if nullness is known + * - IF_ACMPEQ / IF_ACMPNE are rewritten to GOTO if the both references are known null, or if + * one is known null and the other known not-null + * - ALOAD is replaced by ACONST_NULL if the local is known to hold null + * - ASTORE of null is removed if the local is known to hold null + * - INSTANCEOF of null is replaced by `ICONST_0` + * - scala.runtime.BoxesRunTime.unboxToX(null) is rewritten to a zero-value load + */ + def nullnessOptimizations(method: MethodNode, ownerClassName: InternalName): Boolean = { + AsmAnalyzer.sizeOKForNullness(method) && { + lazy val nullnessAnalyzer = new AsmAnalyzer(method, ownerClassName, new NullnessAnalyzer(btypes)) + + // When running nullness optimizations the method may still have unreachable code. Analyzer + // frames of unreachable instructions are `null`. + def frameAt(insn: AbstractInsnNode): Option[Frame[NullnessValue]] = Option(nullnessAnalyzer.frameAt(insn)) + + def nullness(insn: AbstractInsnNode, slot: Int): Option[NullnessValue] = { + frameAt(insn).map(_.getValue(slot)) + } + + def isNull(insn: AbstractInsnNode, slot: Int) = nullness(insn, slot).contains(NullValue) + + // cannot change instructions while iterating, it gets the analysis out of synch (indexed by instructions) + val toReplace = mutable.Map.empty[AbstractInsnNode, List[AbstractInsnNode]] + + val it = method.instructions.iterator() + while (it.hasNext) it.next() match { + case vi: VarInsnNode if isNull(vi, vi.`var`) => + if (vi.getOpcode == ALOAD) + toReplace(vi) = List(new InsnNode(ACONST_NULL)) + else if (vi.getOpcode == ASTORE) + for (frame <- frameAt(vi) if frame.peekStack(0) == NullValue) + toReplace(vi) = List(getPop(1)) + + case ji: JumpInsnNode => + val isIfNull = ji.getOpcode == IFNULL + val isIfNonNull = ji.getOpcode == IFNONNULL + if (isIfNull || isIfNonNull) for (frame <- frameAt(ji)) { + val nullness = frame.peekStack(0) + val taken = nullness == NullValue && isIfNull || nullness == NotNullValue && isIfNonNull + val avoided = nullness == NotNullValue && isIfNull || nullness == NullValue && isIfNonNull + if (taken || avoided) { + val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil + toReplace(ji) = getPop(1) :: jump + } + } else { + val isIfEq = ji.getOpcode == IF_ACMPEQ + val isIfNe = ji.getOpcode == IF_ACMPNE + if (isIfEq || isIfNe) for (frame <- frameAt(ji)) { + val aNullness = frame.peekStack(1) + val bNullness = frame.peekStack(0) + val eq = aNullness == NullValue && bNullness == NullValue + val ne = aNullness == NullValue && bNullness == NotNullValue || aNullness == NotNullValue && bNullness == NullValue + val taken = isIfEq && eq || isIfNe && ne + val avoided = isIfEq && ne || isIfNe && eq + if (taken || avoided) { + val jump = if (taken) List(new JumpInsnNode(GOTO, ji.label)) else Nil + toReplace(ji) = getPop(1) :: getPop(1) :: jump + } + } + } + + case ti: TypeInsnNode => + if (ti.getOpcode == INSTANCEOF) for (frame <- frameAt(ti) if frame.peekStack(0) == NullValue) { + toReplace(ti) = List(getPop(1), new InsnNode(ICONST_0)) + } + + case mi: MethodInsnNode => + if (isScalaUnbox(mi)) for (frame <- frameAt(mi) if frame.peekStack(0) == NullValue) { + toReplace(mi) = List( + getPop(1), + loadZeroForTypeSort(Type.getReturnType(mi.desc).getSort)) + } + + case _ => + } + + def removeFromCallGraph(insn: AbstractInsnNode): Unit = insn match { + case mi: MethodInsnNode => callGraph.removeCallsite(mi, method) + case _ => + } + + for ((oldOp, newOps) <- toReplace) { + for (newOp <- newOps) method.instructions.insertBefore(oldOp, newOp) + method.instructions.remove(oldOp) + removeFromCallGraph(oldOp) + } + + toReplace.nonEmpty + } + } -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 + def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: InternalName): (Boolean, Set[LabelNode]) = { + val a = new AsmAnalyzer(method, ownerClassName) + val frames = a.analyzer.getFrames - val initialSize = method.instructions.size var i = 0 var liveLabels = Set.empty[LabelNode] - var removedInstructions = Set.empty[AbstractInsnNode] + var changed = false + var maxLocals = parametersSize(method) + var maxStack = 0 val itr = method.instructions.iterator() while (itr.hasNext) { - itr.next() match { - case l: LabelNode => - if (frames(i) != null) liveLabels += l + val insn = itr.next() + val isLive = frames(i) != null + if (isLive) maxStack = math.max(maxStack, frames(i).getStackSize) - case ins => + insn match { + case l: LabelNode => // label nodes are not removed: they might be referenced for example in a LocalVariableNode - if (frames(i) == null || ins.getOpcode == Opcodes.NOP) { + if (isLive) liveLabels += l + + case v: VarInsnNode if isLive => + val longSize = if (isSize2LoadOrStore(v.getOpcode)) 1 else 0 + maxLocals = math.max(maxLocals, v.`var` + longSize + 1) // + 1 because local numbers are 0-based + + case i: IincInsnNode if isLive => + maxLocals = math.max(maxLocals, i.`var` + 1) + + case _ => + if (!isLive || insn.getOpcode == NOP) { // Instruction iterators allow removing during iteration. // Removing is O(1): instructions are doubly linked list elements. itr.remove() - removedInstructions += ins + changed = true + insn match { + case invocation: MethodInsnNode => callGraph.removeCallsite(invocation, method) + case indy: InvokeDynamicInsnNode => callGraph.removeClosureInstantiation(indy, method) + case _ => + } } } i += 1 } - (removedInstructions, liveLabels) + method.maxLocals = maxLocals + method.maxStack = maxStack + (changed, liveLabels) } /** + * Eliminate `CHECKCAST` instructions that are statically known to succeed. This is safe if the + * tested object is null: `null.asInstanceOf` always succeeds. + * + * The type of the tested object is determined using a NonLubbingTypeFlowAnalyzer. Note that this + * analysis collapses LUBs of non-equal references types to Object for simplicity. Example: + * given `B <: A <: Object`, the cast in `(if (..) new B else new A).asInstanceOf[A]` would not + * be eliminated. + * + * Note: we cannot replace `INSTANCEOF` tests by only looking at the types, `null.isInstanceOf` + * always returns false, so we'd also need nullness information. + */ + def eliminateRedundantCasts(method: MethodNode, owner: InternalName): Boolean = { + AsmAnalyzer.sizeOKForBasicValue(method) && { + def isSubType(aRefDesc: String, bClass: InternalName): Boolean = aRefDesc == bClass || bClass == ObjectRef.internalName || { + (bTypeForDescriptorOrInternalNameFromClassfile(aRefDesc) conformsTo classBTypeFromParsedClassfile(bClass)).getOrElse(false) + } + + lazy val typeAnalyzer = new NonLubbingTypeFlowAnalyzer(method, owner) + + // cannot remove instructions while iterating, it gets the analysis out of synch (indexed by instructions) + val toRemove = mutable.Set.empty[TypeInsnNode] + + val it = method.instructions.iterator() + while (it.hasNext) it.next() match { + case ti: TypeInsnNode if ti.getOpcode == CHECKCAST => + val frame = typeAnalyzer.frameAt(ti) + val valueTp = frame.getValue(frame.stackTop) + if (valueTp.isReference && isSubType(valueTp.getType.getDescriptor, ti.desc)) { + toRemove += ti + } + + case _ => + } + + toRemove foreach method.instructions.remove + toRemove.nonEmpty + } + } +} + +object LocalOptImpls { + /** * 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. * @@ -235,16 +587,16 @@ 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 | Opcodes.GOTO => containsExecutableCode(start.getNext, end) + case -1 | GOTO => containsExecutableCode(start.getNext, end) case _ => true }) } 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 @@ -263,9 +615,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) }) } @@ -285,17 +638,6 @@ object LocalOptImpls { } /** - * 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. * @@ -310,12 +652,9 @@ object LocalOptImpls { val renumber = collection.mutable.ArrayBuffer.empty[Int] // Add the index of the local variable used by `varIns` to the `renumber` array. - def addVar(varIns: VarInsnNode): Unit = { - val index = varIns.`var` - val isWide = (varIns.getOpcode: @switch) match { - case Opcodes.LLOAD | Opcodes.DLOAD | Opcodes.LSTORE | Opcodes.DSTORE => true - case _ => false - } + def addVar(varIns: AbstractInsnNode, slot: Int): Unit = { + val index = slot + val isWide = isSize2LoadOrStore(varIns.getOpcode) // Ensure the length of `renumber`. Unused variable indices are mapped to -1. val minLength = if (isWide) index + 2 else index + 1 @@ -332,7 +671,7 @@ object LocalOptImpls { val firstLocalIndex = parametersSize(method) for (i <- 0 until firstLocalIndex) renumber += i // parameters and `this` are always used. method.instructions.iterator().asScala foreach { - case VarInstruction(varIns) => addVar(varIns) + case VarInstruction(varIns, slot) => addVar(varIns, slot) case _ => } @@ -353,10 +692,12 @@ object LocalOptImpls { // update variable instructions according to the renumber table method.maxLocals = nextIndex method.instructions.iterator().asScala.foreach { - case VarInstruction(varIns) => - val oldIndex = varIns.`var` - if (oldIndex >= firstLocalIndex && renumber(oldIndex) != oldIndex) - varIns.`var` = renumber(varIns.`var`) + case VarInstruction(varIns, slot) => + val oldIndex = slot + if (oldIndex >= firstLocalIndex && renumber(oldIndex) != oldIndex) varIns match { + case vi: VarInsnNode => vi.`var` = renumber(slot) + case ii: IincInsnNode => ii.`var` = renumber(slot) + } case _ => } true @@ -431,154 +772,173 @@ object LocalOptImpls { // 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 jumpInsns = mutable.LinkedHashMap.empty[JumpInsnNode, Boolean] - val iterator = method.instructions.iterator() - while (iterator.hasNext) { - val instruction = iterator.next() + for (insn <- method.instructions.iterator().asScala) insn match { + case l: LabelNode => + activeHandlers ++= allHandlers.filter(_.start == l) + activeHandlers = activeHandlers.filter(_.end != l) - instruction match { - case l: LabelNode => - activeHandlers ++= allHandlers.filter(_.start == l) - activeHandlers = activeHandlers.filter(_.end != l) - case _ => + case ji: JumpInsnNode => + jumpInsns(ji) = activeHandlers.nonEmpty + + case _ => + } + + var _jumpTargets: Set[AbstractInsnNode] = null + def jumpTargets = { + if (_jumpTargets == null) { + _jumpTargets = jumpInsns.keysIterator.map(_.label).toSet } + _jumpTargets + } - 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) + def removeJumpFromMap(jump: JumpInsnNode) = { + jumpInsns.remove(jump) + _jumpTargets = null + } - if (!jumpRemoved) { - changed = collapseJumpChains(instruction) || changed - jumpRemoved = removeJumpToSuccessor(method, instruction) + def replaceJumpByPop(jump: JumpInsnNode) = { + removeJumpAndAdjustStack(method, jump) + removeJumpFromMap(jump) + } - if (!jumpRemoved) { - val staleGoto = simplifyBranchOverGoto(method, instruction) - instructionsToRemove ++= staleGoto - changed ||= staleGoto.nonEmpty - changed = simplifyGotoReturn(method, instruction, inTryBlock = activeHandlers.nonEmpty) || 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. + */ + def simplifyThenElseSameTarget(insn: AbstractInsnNode): Boolean = insn match { + case ConditionalJump(jump) => + nextExecutableInstruction(insn) match { + case Some(Goto(elseJump)) if sameTargetExecutableInstruction(jump, elseJump) => + replaceJumpByPop(jump) + true + + case _ => false } - changed ||= jumpRemoved - } + + case _ => false } - 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) + /** + * 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. + */ + def collapseJumpChains(insn: AbstractInsnNode): Boolean = insn match { + case JumpNonJsr(jump) => + val target = finalJumpTarget(jump) + if (jump.label == target) false else { + jump.label = target + _jumpTargets = null true + } - case _ => false - } - 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 + /** + * 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. + */ + def removeJumpToSuccessor(insn: AbstractInsnNode): Boolean = insn match { + case JumpNonJsr(jump) if nextExecutableInstruction(jump, alsoKeep = Set(jump.label)) contains jump.label => + replaceJumpByPop(jump) true - } - case _ => false - } + 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 jump targets]; GOTO m; [nops]; l: [...] + * => NegatedCondJump m; [nops, no jump targets]; [nops]; l: [...] + * + * Note that no jump targets are allowed in the first [nops] section. Otherwise, there could + * be some other jump to the GOTO, and eliminating it would change behavior. + */ + def simplifyBranchOverGoto(insn: AbstractInsnNode, inTryBlock: Boolean): Boolean = insn match { + case ConditionalJump(jump) => + // don't skip over jump targets, see doc comment + nextExecutableInstruction(jump, alsoKeep = jumpTargets) match { + case Some(Goto(goto)) => + if (nextExecutableInstruction(goto, alsoKeep = Set(jump.label)) contains jump.label) { + val newJump = new JumpInsnNode(negateJumpOpcode(jump.getOpcode), goto.label) + method.instructions.set(jump, newJump) + removeJumpFromMap(jump) + jumpInsns(newJump) = inTryBlock + replaceJumpByPop(goto) + true + } else false + + case _ => false + } + 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 + */ + def simplifyGotoReturn(instruction: AbstractInsnNode, inTryBlock: Boolean): Boolean = !inTryBlock && (instruction match { + case Goto(jump) => + nextExecutableInstruction(jump.label) match { + case Some(target) => + if (isReturn(target) || target.getOpcode == ATHROW) { + method.instructions.set(jump, target.clone(null)) + removeJumpFromMap(jump) + true + } else false + + case _ => false + } + case _ => false + }) - /** - * 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 + def run(): Boolean = { + var changed = false + + // `.toList` because we're modifying the map while iterating over it + for ((jumpInsn, inTryBlock) <- jumpInsns.toList if jumpInsns.contains(jumpInsn) && isJumpNonJsr(jumpInsn)) { + var jumpRemoved = simplifyThenElseSameTarget(jumpInsn) + + if (!jumpRemoved) { + changed = collapseJumpChains(jumpInsn) || changed + jumpRemoved = removeJumpToSuccessor(jumpInsn) + + if (!jumpRemoved) { + changed = simplifyBranchOverGoto(jumpInsn, inTryBlock) || changed + changed = simplifyGotoReturn(jumpInsn, inTryBlock) || changed + } + } - case _ => false + changed ||= jumpRemoved } - case _ => false - }) + + if (changed) run() + changed + } + + run() + } } |