Skip to content

Commit 90781e8

Browse files
committed
Eliminate unreachable code in GenBCode
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 information for reachable instructions. Instructions for which no data is available after the analyis are unreachable. There's one issue with the ASM framework (or the way we use it): Data flow analysis requires the maxlocals and maxstack of each method to be computed. The ASM framework calculates these maxes only when writing the classfiles, not during code generation. In order to run DCE, we therefore run a MethodWriter beforehand on every method. This assings the MethodNode's maxStack/maxLocals, but it does more work (writes the instructions to a byte array). This is also what Miguel uses on his branch. The change is basically the same as bfadf92c20. We could probably make this faster (and allocate less memory) by hacking the ASM framework: 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. For now, I added some timers to be able to measure the time DCE takes. Here's compiling the library with -Ystatistics:jvm time in backend : 1 spans, 6597ms bcode initialization : 1 spans, 8ms (0.1%) code generation : 1 spans, 4580ms (69.4%) dead code elimination : 3771 spans, 742ms (11.2%) classfile writing : 1 spans, 879ms (13.3%)
1 parent a3dfb43 commit 90781e8

File tree

6 files changed

+134
-4
lines changed

6 files changed

+134
-4
lines changed

src/asm/scala/tools/asm/MethodWriter.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
* @author Eric Bruneton
3838
* @author Eugene Kuleshov
3939
*/
40-
class MethodWriter extends MethodVisitor {
40+
public class MethodWriter extends MethodVisitor {
4141

4242
/**
4343
* Pseudo access flag used to denote constructors.
@@ -235,11 +235,19 @@ class MethodWriter extends MethodVisitor {
235235
*/
236236
private int maxStack;
237237

238+
public int getMaxStack() {
239+
return maxStack;
240+
}
241+
238242
/**
239243
* Maximum number of local variables for this method.
240244
*/
241245
private int maxLocals;
242246

247+
public int getMaxLocals() {
248+
return maxLocals;
249+
}
250+
243251
/**
244252
* Number of local variables in the current stack map frame.
245253
*/

src/compiler/scala/tools/nsc/backend/jvm/BTypes.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,8 @@ abstract class BTypes {
713713
// TODO @lry I don't really understand the reasoning here.
714714
// Both this and other are classes. The code takes (transitively) all superclasses and
715715
// finds the first common one.
716+
// MOST LIKELY the answer can be found here, see the comments and links by Miguel:
717+
// - https://issues.scala-lang.org/browse/SI-3872
716718
firstCommonSuffix(this :: this.superClassesTransitive, other :: other.superClassesTransitive)
717719
}
718720

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/* NSC -- new Scala compiler
2+
* Copyright 2005-2014 LAMP/EPFL
3+
* @author Martin Odersky
4+
*/
5+
6+
package scala.tools.nsc
7+
package backend.jvm
8+
9+
import scala.reflect.internal.util.Statistics
10+
11+
object BackendStats {
12+
import Statistics.{newTimer, newSubTimer}
13+
val bcodeTimer = newTimer("time in backend", "jvm")
14+
15+
val bcodeInitTimer = newSubTimer("bcode initialization", bcodeTimer)
16+
val bcodeGenStat = newSubTimer("code generation", bcodeTimer)
17+
val bcodeDceTimer = newSubTimer("dead code elimination", bcodeTimer)
18+
val bcodeWriteTimer = newSubTimer("classfile writing", bcodeTimer)
19+
}

src/compiler/scala/tools/nsc/backend/jvm/GenBCode.scala

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package jvm
1111

1212
import scala.collection.{ mutable, immutable }
1313
import scala.annotation.switch
14+
import scala.reflect.internal.util.Statistics
1415

1516
import scala.tools.asm
1617

@@ -222,8 +223,12 @@ abstract class GenBCode extends BCodeSyncAndTry {
222223
return
223224
}
224225
else {
225-
try { addToQ3(item) }
226-
catch {
226+
try {
227+
val dceStart = Statistics.startTimer(BackendStats.bcodeDceTimer)
228+
opt.LocalOpt.removeUnreachableCode(item.plain)
229+
Statistics.stopTimer(BackendStats.bcodeDceTimer, dceStart)
230+
addToQ3(item)
231+
} catch {
227232
case ex: Throwable =>
228233
ex.printStackTrace()
229234
error(s"Error while emitting ${item.plain.name}\n${ex.getMessage}")
@@ -272,10 +277,13 @@ abstract class GenBCode extends BCodeSyncAndTry {
272277
*
273278
*/
274279
override def run() {
280+
val bcodeStart = Statistics.startTimer(BackendStats.bcodeTimer)
275281

282+
val initStart = Statistics.startTimer(BackendStats.bcodeInitTimer)
276283
arrivalPos = 0 // just in case
277284
scalaPrimitives.init()
278285
bTypes.intializeCoreBTypes()
286+
Statistics.stopTimer(BackendStats.bcodeInitTimer, initStart)
279287

280288
// initBytecodeWriter invokes fullName, thus we have to run it before the typer-dependent thread is activated.
281289
bytecodeWriter = initBytecodeWriter(cleanup.getEntryPoints)
@@ -287,6 +295,7 @@ abstract class GenBCode extends BCodeSyncAndTry {
287295

288296
// closing output files.
289297
bytecodeWriter.close()
298+
Statistics.stopTimer(BackendStats.bcodeTimer, bcodeStart)
290299

291300
/* TODO Bytecode can be verified (now that all classfiles have been written to disk)
292301
*
@@ -312,9 +321,15 @@ abstract class GenBCode extends BCodeSyncAndTry {
312321
private def buildAndSendToDisk(needsOutFolder: Boolean) {
313322

314323
feedPipeline1()
324+
val genStart = Statistics.startTimer(BackendStats.bcodeGenStat)
315325
(new Worker1(needsOutFolder)).run()
326+
Statistics.stopTimer(BackendStats.bcodeGenStat, genStart)
327+
316328
(new Worker2).run()
329+
330+
val writeStart = Statistics.startTimer(BackendStats.bcodeWriteTimer)
317331
drainQ3()
332+
Statistics.stopTimer(BackendStats.bcodeWriteTimer, writeStart)
318333

319334
}
320335

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/* NSC -- new Scala compiler
2+
* Copyright 2005-2014 LAMP/EPFL
3+
* @author Martin Odersky
4+
*/
5+
6+
package scala.tools.nsc
7+
package backend.jvm
8+
package opt
9+
10+
import scala.tools.asm.{MethodWriter, ClassWriter}
11+
import scala.tools.asm.tree.analysis.{Analyzer, BasicValue, BasicInterpreter}
12+
import scala.tools.asm.tree.{LabelNode, ClassNode, MethodNode}
13+
import scala.collection.convert.decorateAsScala._
14+
15+
/**
16+
* Intra-Method optimizations.
17+
*/
18+
object LocalOpt {
19+
/**
20+
* Remove unreachable instructions from all (non-abstract) methods.
21+
*
22+
* @param clazz The class whose methods are optimized
23+
* @return `true` if unreachable code was elminated in some method, `false` otherwise.
24+
*/
25+
def removeUnreachableCode(clazz: ClassNode): Boolean = {
26+
clazz.methods.asScala.foldLeft(false) {
27+
case (changed, method) => removeUnreachableCode(method, clazz) || changed
28+
}
29+
}
30+
31+
/**
32+
* Remove unreachable code from a method.
33+
* We rely on dead code elimination provided by the ASM framework, as described in the ASM User
34+
* Guide (http://asm.ow2.org/index.html), Section 8.2.1. It runs a data flow analysis, which only
35+
* computes Frame information for reachable instructions. Instructions for which no Frame data is
36+
* available after the analyis are unreachable.
37+
*/
38+
private def removeUnreachableCode(method: MethodNode, ownerClass: ClassNode): Boolean = {
39+
val initialSize = method.instructions.size
40+
if (initialSize == 0) return false
41+
42+
// The data flow analysis requires the maxLocals / maxStack fields of the method to be computed.
43+
computeMaxLocalsMaxStack(method)
44+
val a = new Analyzer[BasicValue](new BasicInterpreter)
45+
a.analyze(ownerClass.name, method)
46+
val frames = a.getFrames
47+
48+
var i = 0
49+
val itr = method.instructions.iterator()
50+
while (itr.hasNext) {
51+
val ins = itr.next()
52+
// Don't remove label nodes: they might be referenced for example in a LocalVariableNode
53+
if (frames(i) == null && !ins.isInstanceOf[LabelNode]) {
54+
// Instruction iterators allow removing during iteration.
55+
// Removing is O(1): instructions are doubly linked list elements.
56+
itr.remove()
57+
}
58+
i += 1
59+
}
60+
61+
method.instructions.size == initialSize
62+
}
63+
64+
/**
65+
* In order to run an Analyzer, the maxLocals / maxStack fields need to be available. The ASM
66+
* framework only computes these values during bytecode generation.
67+
*
68+
* Sicne there's currently no better way, we run a bytecode generator on the method and extract
69+
* the computed values. This required changes to the ASM codebase:
70+
* - the [[MethodWriter]] class was made public
71+
* - accessors for maxLocals / maxStack were added to the MethodWriter class
72+
*
73+
* We could probably make this faster (and allocate less memory) by hacking the ASM framework
74+
* more: create a subclass of MethodWriter with a /dev/null byteVector. Another option would be
75+
* to create a separate visitor for computing those values, duplicating the functionality from the
76+
* MethodWriter.
77+
*/
78+
private def computeMaxLocalsMaxStack(method: MethodNode) {
79+
val cw = new ClassWriter(ClassWriter.COMPUTE_MAXS)
80+
val excs = method.exceptions.asScala.toArray
81+
val mw = cw.visitMethod(method.access, method.name, method.desc, method.signature, excs).asInstanceOf[MethodWriter]
82+
method.accept(mw)
83+
method.maxLocals = mw.getMaxLocals
84+
method.maxStack = mw.getMaxStack
85+
}
86+
}

src/compiler/scala/tools/nsc/settings/ScalaSettings.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ trait ScalaSettings extends AbsScalaSettings
211211

212212
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."
213213

214-
object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup = Value }
214+
object YstatisticsPhases extends MultiChoiceEnumeration { val parser, typer, patmat, erasure, cleanup, jvm = Value }
215215
val Ystatistics = {
216216
val description = "Print compiler statistics for specific phases"
217217
MultiChoiceSetting(

0 commit comments

Comments
 (0)