Skip to content

Commit 865aa63

Browse files
authored
Some performance related changes (#16566)
A series of optimizations to reduce allocation rates and speed up compilation. I think in the end we got 5-10% depending on benchmark. The optimizations were driven by taking close looks at allocation and cpu profiles generated by async-profiler.
2 parents 783dc13 + f2caf05 commit 865aa63

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+708
-479
lines changed

compiler/src/dotty/tools/dotc/Bench.scala

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,22 @@ import scala.annotation.internal.sharable
1414
object Bench extends Driver:
1515

1616
@sharable private var numRuns = 1
17-
18-
private def ntimes(n: Int)(op: => Reporter): Reporter =
19-
(0 until n).foldLeft(emptyReporter)((_, _) => op)
20-
17+
@sharable private var numCompilers = 1
18+
@sharable private var waitAfter = -1
19+
@sharable private var curCompiler = 0
2120
@sharable private var times: Array[Int] = _
2221

2322
override def doCompile(compiler: Compiler, files: List[AbstractFile])(using Context): Reporter =
24-
times = new Array[Int](numRuns)
2523
var reporter: Reporter = emptyReporter
2624
for i <- 0 until numRuns do
25+
val curRun = curCompiler * numRuns + i
2726
val start = System.nanoTime()
2827
reporter = super.doCompile(compiler, files)
29-
times(i) = ((System.nanoTime - start) / 1000000).toInt
30-
println(s"time elapsed: ${times(i)}ms")
31-
if ctx.settings.Xprompt.value then
28+
times(curRun) = ((System.nanoTime - start) / 1000000).toInt
29+
println(s"time elapsed: ${times(curRun)}ms")
30+
if ctx.settings.Xprompt.value || waitAfter == curRun + 1 then
3231
print("hit <return> to continue >")
3332
System.in.nn.read()
34-
println()
3533
reporter
3634

3735
def extractNumArg(args: Array[String], name: String, default: Int = 1): (Int, Array[String]) = {
@@ -42,20 +40,26 @@ object Bench extends Driver:
4240

4341
def reportTimes() =
4442
val best = times.sorted
45-
val measured = numRuns / 3
43+
val measured = numCompilers * numRuns / 3
4644
val avgBest = best.take(measured).sum / measured
4745
val avgLast = times.reverse.take(measured).sum / measured
48-
println(s"best out of $numRuns runs: ${best(0)}")
46+
println(s"best out of ${numCompilers * numRuns} runs: ${best(0)}")
4947
println(s"average out of best $measured: $avgBest")
5048
println(s"average out of last $measured: $avgLast")
5149

52-
override def process(args: Array[String], rootCtx: Context): Reporter =
50+
override def process(args: Array[String]): Reporter =
5351
val (numCompilers, args1) = extractNumArg(args, "#compilers")
5452
val (numRuns, args2) = extractNumArg(args1, "#runs")
53+
val (waitAfter, args3) = extractNumArg(args2, "#wait-after", -1)
54+
this.numCompilers = numCompilers
5555
this.numRuns = numRuns
56+
this.waitAfter = waitAfter
57+
this.times = new Array[Int](numCompilers * numRuns)
5658
var reporter: Reporter = emptyReporter
57-
for i <- 0 until numCompilers do
58-
reporter = super.process(args2, rootCtx)
59+
curCompiler = 0
60+
while curCompiler < numCompilers do
61+
reporter = super.process(args3)
62+
curCompiler += 1
5963
reportTimes()
6064
reporter
6165

compiler/src/dotty/tools/dotc/Driver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ class Driver {
171171
* the other overloads without worrying about breaking compatibility
172172
* with sbt.
173173
*/
174-
final def process(args: Array[String]): Reporter =
174+
def process(args: Array[String]): Reporter =
175175
process(args, null: Reporter | Null, null: interfaces.CompilerCallback | Null)
176176

177177
/** Entry point to the compiler using a custom `Context`.

compiler/src/dotty/tools/dotc/Run.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,9 @@ class Run(comp: Compiler, ictx: Context) extends ImplicitRunInfo with Constraint
231231
ctx.settings.Yskip.value, ctx.settings.YstopBefore.value, stopAfter, ctx.settings.Ycheck.value)
232232
ctx.base.usePhases(phases)
233233

234+
if ctx.settings.YnoDoubleBindings.value then
235+
ctx.base.checkNoDoubleBindings = true
236+
234237
def runPhases(using Context) = {
235238
var lastPrintedTree: PrintedTree = NoPrintedTree
236239
val profiler = ctx.profiler

compiler/src/dotty/tools/dotc/ast/Positioned.scala

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,17 @@ abstract class Positioned(implicit @constructorOnly src: SourceFile) extends Src
154154
}
155155
}
156156

157+
private class LastPosRef:
158+
var positioned: Positioned | Null = null
159+
var span = NoSpan
160+
157161
/** Check that all positioned items in this tree satisfy the following conditions:
158162
* - Parent spans contain child spans
159163
* - If item is a non-empty tree, it has a position
160164
*/
161165
def checkPos(nonOverlapping: Boolean)(using Context): Unit = try {
162166
import untpd._
163-
var lastPositioned: Positioned | Null = null
164-
var lastSpan = NoSpan
167+
val last = LastPosRef()
165168
def check(p: Any): Unit = p match {
166169
case p: Positioned =>
167170
assert(span contains p.span,
@@ -181,19 +184,19 @@ abstract class Positioned(implicit @constructorOnly src: SourceFile) extends Src
181184
case _: XMLBlock =>
182185
// FIXME: Trees generated by the XML parser do not satisfy `checkPos`
183186
case _: WildcardFunction
184-
if lastPositioned.isInstanceOf[ValDef] && !p.isInstanceOf[ValDef] =>
187+
if last.positioned.isInstanceOf[ValDef] && !p.isInstanceOf[ValDef] =>
185188
// ignore transition from last wildcard parameter to body
186189
case _ =>
187-
assert(!lastSpan.exists || !p.span.exists || lastSpan.end <= p.span.start,
190+
assert(!last.span.exists || !p.span.exists || last.span.end <= p.span.start,
188191
i"""position error, child positions overlap or in wrong order
189192
|parent = $this
190-
|1st child = $lastPositioned
191-
|1st child span = $lastSpan
193+
|1st child = ${last.positioned}
194+
|1st child span = ${last.span}
192195
|2nd child = $p
193196
|2nd child span = ${p.span}""".stripMargin)
194197
}
195-
lastPositioned = p
196-
lastSpan = p.span
198+
last.positioned = p
199+
last.span = p.span
197200
p.checkPos(nonOverlapping)
198201
case m: untpd.Modifiers =>
199202
m.annotations.foreach(check)

compiler/src/dotty/tools/dotc/ast/TreeInfo.scala

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -830,10 +830,12 @@ trait TypedTreeInfo extends TreeInfo[Type] { self: Trees.Instance[Type] =>
830830

831831
/** The symbols defined locally in a statement list */
832832
def localSyms(stats: List[Tree])(using Context): List[Symbol] =
833-
val locals = new mutable.ListBuffer[Symbol]
834-
for stat <- stats do
835-
if stat.isDef && stat.symbol.exists then locals += stat.symbol
836-
locals.toList
833+
if stats.isEmpty then Nil
834+
else
835+
val locals = new mutable.ListBuffer[Symbol]
836+
for stat <- stats do
837+
if stat.isDef && stat.symbol.exists then locals += stat.symbol
838+
locals.toList
837839

838840
/** If `tree` is a DefTree, the symbol defined by it, otherwise NoSymbol */
839841
def definedSym(tree: Tree)(using Context): Symbol =

compiler/src/dotty/tools/dotc/ast/tpd.scala

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -857,7 +857,7 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
857857
}
858858

859859
/** After phase `trans`, set the owner of every definition in this tree that was formerly
860-
* owner by `from` to `to`.
860+
* owned by `from` to `to`.
861861
*/
862862
def changeOwnerAfter(from: Symbol, to: Symbol, trans: DenotTransformer)(using Context): ThisTree =
863863
if (ctx.phase == trans.next) {
@@ -1144,35 +1144,38 @@ object tpd extends Trees.Instance[Type] with TypedTreeInfo {
11441144
expand(tree, tree.tpe.widen)
11451145
}
11461146

1147-
inline val MapRecursionLimit = 10
1148-
11491147
extension (trees: List[Tree])
11501148

1151-
/** A map that expands to a recursive function. It's equivalent to
1149+
/** Equivalent (but faster) to
11521150
*
11531151
* flatten(trees.mapConserve(op))
11541152
*
1155-
* and falls back to it after `MaxRecursionLimit` recursions.
1156-
* Before that it uses a simpler method that uses stackspace
1157-
* instead of heap.
1158-
* Note `op` is duplicated in the generated code, so it should be
1159-
* kept small.
1153+
* assuming that `trees` does not contain `Thicket`s to start with.
11601154
*/
1161-
inline def mapInline(inline op: Tree => Tree): List[Tree] =
1162-
def recur(trees: List[Tree], count: Int): List[Tree] =
1163-
if count > MapRecursionLimit then
1164-
// use a slower implementation that avoids stack overflows
1165-
flatten(trees.mapConserve(op))
1166-
else trees match
1167-
case tree :: rest =>
1168-
val tree1 = op(tree)
1169-
val rest1 = recur(rest, count + 1)
1170-
if (tree1 eq tree) && (rest1 eq rest) then trees
1171-
else tree1 match
1172-
case Thicket(elems1) => elems1 ::: rest1
1173-
case _ => tree1 :: rest1
1174-
case nil => nil
1175-
recur(trees, 0)
1155+
inline def flattenedMapConserve(inline f: Tree => Tree): List[Tree] =
1156+
@tailrec
1157+
def loop(mapped: ListBuffer[Tree] | Null, unchanged: List[Tree], pending: List[Tree]): List[Tree] =
1158+
if pending.isEmpty then
1159+
if mapped == null then unchanged
1160+
else mapped.prependToList(unchanged)
1161+
else
1162+
val head0 = pending.head
1163+
val head1 = f(head0)
1164+
1165+
if head1 eq head0 then
1166+
loop(mapped, unchanged, pending.tail)
1167+
else
1168+
val buf = if mapped == null then new ListBuffer[Tree] else mapped
1169+
var xc = unchanged
1170+
while xc ne pending do
1171+
buf += xc.head
1172+
xc = xc.tail
1173+
head1 match
1174+
case Thicket(elems1) => buf ++= elems1
1175+
case _ => buf += head1
1176+
val tail0 = pending.tail
1177+
loop(buf, tail0, tail0)
1178+
loop(null, trees, trees)
11761179

11771180
/** Transform statements while maintaining import contexts and expression contexts
11781181
* in the same way as Typer does. The code addresses additional concerns:

compiler/src/dotty/tools/dotc/config/Config.scala

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,6 @@ object Config {
8383
*/
8484
inline val failOnInstantiationToNothing = false
8585

86-
/** Enable noDoubleDef checking if option "-YnoDoubleDefs" is set.
87-
* The reason to have an option as well as the present global switch is
88-
* that the noDoubleDef checking is done in a hotspot, and we do not
89-
* want to incur the overhead of checking an option each time.
90-
*/
91-
inline val checkNoDoubleBindings = true
92-
9386
/** Check positions for consistency after parsing */
9487
inline val checkPositions = true
9588

0 commit comments

Comments
 (0)