summaryrefslogtreecommitdiff
path: root/src/compiler
diff options
context:
space:
mode:
authorJason Zaugg <jzaugg@gmail.com>2014-09-16 14:57:23 +1000
committerJason Zaugg <jzaugg@gmail.com>2014-09-16 14:57:23 +1000
commit0e2be38f05a6d3fadd0a6c800604503c72401117 (patch)
treea9b836b0ec7048d6182448d1157f99711703bf05 /src/compiler
parent1b9806171940d304b41442b788717d2425764cbf (diff)
parent9132efa4a8511e267c808c95df4d2e3de68277e6 (diff)
downloadscala-0e2be38f05a6d3fadd0a6c800604503c72401117.tar.gz
scala-0e2be38f05a6d3fadd0a6c800604503c72401117.tar.bz2
scala-0e2be38f05a6d3fadd0a6c800604503c72401117.zip
Merge pull request #3971 from lrytz/opt/dce
GenBCode: eliminate unreachable code
Diffstat (limited to 'src/compiler')
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala50
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala9
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala7
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala2
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/BackendStats.scala24
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala26
-rw-r--r--src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala190
-rw-r--r--src/compiler/scala/tools/nsc/settings/ScalaSettings.scala28
8 files changed, 323 insertions, 13 deletions
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala
index 397171049f..daf36ce374 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeBodyBuilder.scala
@@ -9,7 +9,6 @@ package tools.nsc
package backend
package jvm
-import scala.collection.{ mutable, immutable }
import scala.annotation.switch
import scala.tools.asm
@@ -283,9 +282,10 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
val Local(tk, _, idx, isSynth) = locals.getOrMakeLocal(sym)
if (rhs == EmptyTree) { emitZeroOf(tk) }
else { genLoad(rhs, tk) }
+ val localVarStart = currProgramPoint()
bc.store(idx, tk)
if (!isSynth) { // there are case <synthetic> ValDef's emitted by patmat
- varsInScope ::= (sym -> currProgramPoint())
+ varsInScope ::= (sym -> localVarStart)
}
generatedType = UNIT
@@ -815,7 +815,51 @@ abstract class BCodeBodyBuilder extends BCodeSkelBuilder {
case _ => bc.emitT2T(from, to)
}
} else if (from.isNothingType) {
- emit(asm.Opcodes.ATHROW) // ICode enters here into enterIgnoreMode, we'll rely instead on DCE at ClassNode level.
+ /* There are two possibilities for from.isNothingType: emitting a "throw e" expressions and
+ * loading a (phantom) value of type Nothing.
+ *
+ * The Nothing type in Scala's type system does not exist in the JVM. In bytecode, Nothing
+ * is mapped to scala.runtime.Nothing$. To the JVM, a call to Predef.??? looks like it would
+ * return an object of type Nothing$. We need to do something with that phantom object on
+ * the stack. "Phantom" because it never exists: such methods always throw, but the JVM does
+ * not know that.
+ *
+ * Note: The two verifiers (old: type inference, new: type checking) have different
+ * requirements. Very briefly:
+ *
+ * Old (http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.2.1): at
+ * each program point, no matter what branches were taken to get there
+ * - Stack is same size and has same typed values
+ * - Local and stack values need to have consistent types
+ * - In practice, the old verifier seems to ignore unreachable code and accept any
+ * instructions after an ATHROW. For example, there can be another ATHROW (without
+ * loading another throwable first).
+ *
+ * New (http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.1)
+ * - Requires consistent stack map frames. GenBCode generates stack frames if -target:jvm-1.6
+ * or higher.
+ * - In practice: the ASM library computes stack map frames for us (ClassWriter). Emitting
+ * correct frames after an ATHROW is probably complex, so ASM uses the following strategy:
+ * - Every time when generating an ATHROW, a new basic block is started.
+ * - During classfile writing, such basic blocks are found to be dead: no branches go there
+ * - Eliminating dead code would probably require complex shifts in the output byte buffer
+ * - But there's an easy solution: replace all code in the dead block with with
+ * `nop; nop; ... nop; athrow`, making sure the bytecode size stays the same
+ * - The corresponding stack frame can be easily generated: on entering a dead the block,
+ * the frame requires a single Throwable on the stack.
+ * - Since there are no branches to the dead block, the frame requirements are never violated.
+ *
+ * To summarize the above: it does matter what we emit after an ATHROW.
+ *
+ * NOW: if we end up here because we emitted a load of a (phantom) value of type Nothing$,
+ * there was no ATHROW emitted. So, we have to make the verifier happy and do something
+ * with that value. Since Nothing$ extends Throwable, the easiest is to just emit an ATHROW.
+ *
+ * If we ended up here because we generated a "throw e" expression, we know the last
+ * emitted instruction was an ATHROW. As explained above, it is OK to emit a second ATHROW,
+ * the verifiers will be happy.
+ */
+ emit(asm.Opcodes.ATHROW)
} else if (from.isNullType) {
bc drop from
emit(asm.Opcodes.ACONST_NULL)
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala
index 5670715cd3..14bffd67ab 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeHelpers.scala
@@ -782,14 +782,9 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
}
} // end of trait BCClassGen
- /* basic functionality for class file building of plain, mirror, and beaninfo classes. */
- abstract class JBuilder extends BCInnerClassGen {
-
- } // end of class JBuilder
-
/* functionality for building plain and mirror classes */
abstract class JCommonBuilder
- extends JBuilder
+ extends BCInnerClassGen
with BCAnnotGen
with BCForwardersGen
with BCPickles { }
@@ -851,7 +846,7 @@ abstract class BCodeHelpers extends BCodeIdiomatic with BytecodeWriters {
} // end of class JMirrorBuilder
/* builder of bean info classes */
- class JBeanInfoBuilder extends JBuilder {
+ class JBeanInfoBuilder extends BCInnerClassGen {
/*
* Generate a bean info class that describes the given class.
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala
index 4592031a31..03bc32061b 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BCodeSkelBuilder.scala
@@ -346,6 +346,13 @@ abstract class BCodeSkelBuilder extends BCodeHelpers {
/*
* Bookkeeping for method-local vars and method-params.
+ *
+ * TODO: use fewer slots. local variable slots are never re-used in separate blocks.
+ * In the following example, x and y could use the same slot.
+ * def foo() = {
+ * { val x = 1 }
+ * { val y = "a" }
+ * }
*/
object locals {
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala
index 7bf61b4f51..53ac5bfdc7 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala
@@ -713,6 +713,8 @@ abstract class BTypes {
// TODO @lry I don't really understand the reasoning here.
// Both this and other are classes. The code takes (transitively) all superclasses and
// finds the first common one.
+ // MOST LIKELY the answer can be found here, see the comments and links by Miguel:
+ // - https://issues.scala-lang.org/browse/SI-3872
firstCommonSuffix(this :: this.superClassesTransitive, other :: other.superClassesTransitive)
}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/BackendStats.scala b/src/compiler/scala/tools/nsc/backend/jvm/BackendStats.scala
new file mode 100644
index 0000000000..4b9383c67c
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/backend/jvm/BackendStats.scala
@@ -0,0 +1,24 @@
+/* NSC -- new Scala compiler
+ * Copyright 2005-2014 LAMP/EPFL
+ * @author Martin Odersky
+ */
+
+package scala.tools.nsc
+package backend.jvm
+
+import scala.reflect.internal.util.Statistics
+
+object BackendStats {
+ import Statistics.{newTimer, newSubTimer}
+ val bcodeTimer = newTimer("time in backend", "jvm")
+
+ val bcodeInitTimer = newSubTimer("bcode initialization", bcodeTimer)
+ val bcodeGenStat = newSubTimer("code generation", bcodeTimer)
+ val bcodeDceTimer = newSubTimer("dead code elimination", bcodeTimer)
+ val bcodeWriteTimer = newSubTimer("classfile writing", bcodeTimer)
+
+ def timed[T](timer: Statistics.Timer)(body: => T): T = {
+ val start = Statistics.startTimer(timer)
+ try body finally Statistics.stopTimer(timer, start)
+ }
+}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala
index 0a7c894a69..ba94a9c44c 100644
--- a/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala
+++ b/src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala
@@ -11,8 +11,10 @@ package jvm
import scala.collection.{ mutable, immutable }
import scala.annotation.switch
+import scala.reflect.internal.util.Statistics
import scala.tools.asm
+import scala.tools.asm.tree.ClassNode
/*
* Prepare in-memory representations of classfiles using the ASM Tree API, and serialize them to disk.
@@ -213,6 +215,14 @@ abstract class GenBCode extends BCodeSyncAndTry {
* - converting the plain ClassNode to byte array and placing it on queue-3
*/
class Worker2 {
+ def localOptimizations(classNode: ClassNode): Unit = {
+ def dce(): Boolean = BackendStats.timed(BackendStats.bcodeDceTimer) {
+ if (settings.YoptUnreachableCode) opt.LocalOpt.removeUnreachableCode(classNode)
+ else false
+ }
+
+ dce()
+ }
def run() {
while (true) {
@@ -222,8 +232,10 @@ abstract class GenBCode extends BCodeSyncAndTry {
return
}
else {
- try { addToQ3(item) }
- catch {
+ try {
+ localOptimizations(item.plain)
+ addToQ3(item)
+ } catch {
case ex: Throwable =>
ex.printStackTrace()
error(s"Error while emitting ${item.plain.name}\n${ex.getMessage}")
@@ -272,10 +284,13 @@ abstract class GenBCode extends BCodeSyncAndTry {
*
*/
override def run() {
+ val bcodeStart = Statistics.startTimer(BackendStats.bcodeTimer)
+ val initStart = Statistics.startTimer(BackendStats.bcodeInitTimer)
arrivalPos = 0 // just in case
scalaPrimitives.init()
bTypes.intializeCoreBTypes()
+ Statistics.stopTimer(BackendStats.bcodeInitTimer, initStart)
// initBytecodeWriter invokes fullName, thus we have to run it before the typer-dependent thread is activated.
bytecodeWriter = initBytecodeWriter(cleanup.getEntryPoints)
@@ -287,6 +302,7 @@ abstract class GenBCode extends BCodeSyncAndTry {
// closing output files.
bytecodeWriter.close()
+ Statistics.stopTimer(BackendStats.bcodeTimer, bcodeStart)
/* TODO Bytecode can be verified (now that all classfiles have been written to disk)
*
@@ -312,9 +328,15 @@ abstract class GenBCode extends BCodeSyncAndTry {
private def buildAndSendToDisk(needsOutFolder: Boolean) {
feedPipeline1()
+ val genStart = Statistics.startTimer(BackendStats.bcodeGenStat)
(new Worker1(needsOutFolder)).run()
+ Statistics.stopTimer(BackendStats.bcodeGenStat, genStart)
+
(new Worker2).run()
+
+ val writeStart = Statistics.startTimer(BackendStats.bcodeWriteTimer)
drainQ3()
+ Statistics.stopTimer(BackendStats.bcodeWriteTimer, writeStart)
}
diff --git a/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala
new file mode 100644
index 0000000000..3acd2d6154
--- /dev/null
+++ b/src/compiler/scala/tools/nsc/backend/jvm/opt/LocalOpt.scala
@@ -0,0 +1,190 @@
+/* NSC -- new Scala compiler
+ * Copyright 2005-2014 LAMP/EPFL
+ * @author Martin Odersky
+ */
+
+package scala.tools.nsc
+package backend.jvm
+package opt
+
+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.collection.{ mutable => m }
+
+/**
+ * Intra-Method optimizations.
+ */
+object LocalOpt {
+ /**
+ * Remove unreachable instructions from all (non-abstract) methods.
+ *
+ * @param clazz The class whose methods are optimized
+ * @return `true` if unreachable code was elminated in some method, `false` otherwise.
+ */
+ def removeUnreachableCode(clazz: ClassNode): Boolean = {
+ clazz.methods.asScala.foldLeft(false) {
+ case (changed, method) => removeUnreachableCode(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.
+ *
+ * TODO doc: it also removes empty handlers, unused local vars
+ *
+ * Returns `true` if dead code in `method` has been eliminated.
+ */
+ private def removeUnreachableCode(method: MethodNode, ownerClassName: String): Boolean = {
+ if (method.instructions.size == 0) return false // fast path for abstract methods
+
+ val codeRemoved = removeUnreachableCodeImpl(method, ownerClassName)
+
+ // unreachable-code also removes unused local variable nodes and empty exception handlers.
+ // This is required for correctness: such nodes are not allowed to refer to instruction offsets
+ // that don't exist (because they have been eliminated).
+ val localsRemoved = removeUnusedLocalVariableNodes(method)
+ val handlersRemoved = removeEmptyExceptionHandlers(method)
+
+ // When eliminating a handler, the catch block becomes unreachable. The recursive invocation
+ // removes these blocks.
+ // Note that invoking removeUnreachableCode*Impl* a second time is not enough: removing the dead
+ // catch block can render other handlers empty, which also have to be removed in turn.
+ if (handlersRemoved) removeUnreachableCode(method, ownerClassName)
+
+ // assert that we can leave local variable annotations as-is
+ def nullOrEmpty[T](l: java.util.List[T]) = l == null || l.isEmpty
+ assert(nullOrEmpty(method.visibleLocalVariableAnnotations), method.visibleLocalVariableAnnotations)
+ assert(nullOrEmpty(method.invisibleLocalVariableAnnotations), method.invisibleLocalVariableAnnotations)
+
+ codeRemoved || localsRemoved || handlersRemoved
+ }
+
+ private def removeUnreachableCodeImpl(method: MethodNode, ownerClassName: String): Boolean = {
+ val initialSize = method.instructions.size
+ if (initialSize == 0) return false
+
+ // 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
+
+ var i = 0
+ val itr = method.instructions.iterator()
+ while (itr.hasNext) {
+ val ins = itr.next()
+ // Don't remove label nodes: they might be referenced for example in a LocalVariableNode
+ if (frames(i) == null && !ins.isInstanceOf[LabelNode]) {
+ // Instruction iterators allow removing during iteration.
+ // Removing is O(1): instructions are doubly linked list elements.
+ itr.remove()
+ }
+ i += 1
+ }
+
+ method.instructions.size != initialSize
+ }
+
+ /**
+ * Remove exception handlers that cover empty code blocks from all methods of `clazz`.
+ * Returns `true` if any exception handler was eliminated.
+ */
+ def removeEmptyExceptionHandlers(clazz: ClassNode): Boolean = {
+ clazz.methods.asScala.foldLeft(false) {
+ case (changed, method) => removeEmptyExceptionHandlers(method) || changed
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * Note that no instructions are eliminated.
+ *
+ * @return `true` if some exception handler was eliminated.
+ */
+ def removeEmptyExceptionHandlers(method: MethodNode): Boolean = {
+ /** True if there exists code between start and end. */
+ def containsExecutableCode(start: AbstractInsnNode, end: LabelNode): Boolean = {
+ start != end && (start.getOpcode match {
+ // FrameNode, LabelNode and LineNumberNode have opcode == -1.
+ case -1 | Opcodes.NOP | Opcodes.GOTO => containsExecutableCode(start.getNext, end)
+ case _ => true
+ })
+ }
+
+ val initialNumberHandlers = method.tryCatchBlocks.size
+ val handlersIter = method.tryCatchBlocks.iterator()
+ while(handlersIter.hasNext) {
+ val handler = handlersIter.next()
+ if (!containsExecutableCode(handler.start, handler.end)) handlersIter.remove()
+ }
+ method.tryCatchBlocks.size != initialNumberHandlers
+ }
+
+ /**
+ * 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.
+ *
+ * TODO: also re-allocate locals to occupy fewer slots after eliminating unused ones
+ */
+ def removeUnusedLocalVariableNodes(method: MethodNode): Boolean = {
+ def variableIsUsed(start: AbstractInsnNode, end: LabelNode, varIndex: Int): Boolean = {
+ start != end && (start match {
+ case v: VarInsnNode => v.`var` == varIndex
+ case _ => variableIsUsed(start.getNext, end, varIndex)
+ })
+ }
+
+ val initialNumVars = method.localVariables.size
+ val localsIter = method.localVariables.iterator()
+ // The parameters and `this` (for instance methods) have the lowest indices in the local variables
+ // table. Note that 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).map(_.getSize max 1).sum
+ val thisSize = if ((method.access & Opcodes.ACC_STATIC) == 0) 1 else 0
+ val endParamIndex = paramsSize + thisSize
+ while (localsIter.hasNext) {
+ val local = localsIter.next()
+ // parameters and `this` have the lowest indices, starting at 0
+ val used = local.index < endParamIndex || variableIsUsed(local.start, local.end, local.index)
+ if (!used)
+ localsIter.remove()
+ }
+ method.localVariables.size == initialNumVars
+ }
+
+
+ /**
+ * 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
+ }
+}
diff --git a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
index 91b03869e5..466e397dd7 100644
--- a/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
+++ b/src/compiler/scala/tools/nsc/settings/ScalaSettings.scala
@@ -209,9 +209,35 @@ trait ScalaSettings extends AbsScalaSettings
// the current standard is "inline" but we are moving towards "method"
val Ydelambdafy = ChoiceSetting ("-Ydelambdafy", "strategy", "Strategy used for translating lambdas into JVM code.", List("inline", "method"), "inline")
+ object YoptChoices extends MultiChoiceEnumeration {
+ val unreachableCode = Choice("unreachable-code", "Eliminate unreachable code")
+
+ val lNone = Choice("l:none", "Don't enable any optimizations")
+
+ private val defaultChoices = List(unreachableCode)
+ val lDefault = Choice("l:default", "Enable default optimizations: "+ defaultChoices.mkString(","), expandsTo = defaultChoices)
+
+ private val methodChoices = List(lDefault)
+ val lMethod = Choice("l:method", "Intra-method optimizations: "+ methodChoices.mkString(","), expandsTo = methodChoices)
+
+ private val projectChoices = List(lMethod)
+ val lProject = Choice("l:project", "Cross-method optimizations within the current project: "+ projectChoices.mkString(","), expandsTo = projectChoices)
+
+ private val classpathChoices = List(lProject)
+ val lClasspath = Choice("l:classpath", "Cross-method optmizations across the entire classpath: "+ classpathChoices.mkString(","), expandsTo = classpathChoices)
+ }
+
+ val Yopt = MultiChoiceSetting(
+ name = "-Yopt",
+ helpArg = "optimization",
+ descr = "Enable optimizations",
+ domain = YoptChoices)
+
+ def YoptUnreachableCode: Boolean = !Yopt.isSetByUser || Yopt.contains(YoptChoices.unreachableCode)
+
private def removalIn212 = "This flag is scheduled for removal in 2.12. If you have a case where you need this flag then please report a bug."
- object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup = Value }
+ object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup, jvm = Value }
val Ystatistics = {
val description = "Print compiler statistics for specific phases"
MultiChoiceSetting(