Skip to content

Commit 48a7871

Browse files
authored
ExtractDependencies uses more efficient caching (#18403)
alternative to #18219 reduces allocations in the phase by 77% (on `scala3-compiler-bootstrapped`), allocations in the whole compilation pipeline are reduced from about 12% to 4% contributed by this phase. Reduced allocations come from: - reusing a single set for the `seen` cache. - use linear probe maps (dotty.tools.dotc.util) avoiding allocation of nodes - no `ClassDependency` allocation each time a dependency exists (many are likely duplicated) performance is also improved due to less expensive hashing (`EqHashMap`), and introduction of `EqHashSet`, also reducing number of times we hash (`add` method on HashSet, and optimised `getOrElseUpdate`). Probably also from reduced heap pressure of allocating. Now the majority of allocation for this phase is by calling into Zinc
2 parents 1551eab + 8113165 commit 48a7871

13 files changed

+927
-152
lines changed

compiler/src/dotty/tools/dotc/sbt/ExtractDependencies.scala

+76-51
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import xsbti.UseScope
2626
import xsbti.api.DependencyContext
2727
import xsbti.api.DependencyContext._
2828

29+
import scala.jdk.CollectionConverters.*
30+
2931
import scala.collection.{Set, mutable}
3032

3133

@@ -74,8 +76,8 @@ class ExtractDependencies extends Phase {
7476
collector.traverse(unit.tpdTree)
7577

7678
if (ctx.settings.YdumpSbtInc.value) {
77-
val deps = rec.classDependencies.map(_.toString).toArray[Object]
78-
val names = rec.usedNames.map { case (clazz, names) => s"$clazz: $names" }.toArray[Object]
79+
val deps = rec.foundDeps.iterator.map { case (clazz, found) => s"$clazz: ${found.classesString}" }.toArray[Object]
80+
val names = rec.foundDeps.iterator.map { case (clazz, found) => s"$clazz: ${found.namesString}" }.toArray[Object]
7981
Arrays.sort(deps)
8082
Arrays.sort(names)
8183

@@ -162,7 +164,7 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
162164

163165

164166
/** Traverse the tree of a source file and record the dependencies and used names which
165-
* can be retrieved using `dependencies` and`usedNames`.
167+
* can be retrieved using `foundDeps`.
166168
*/
167169
override def traverse(tree: Tree)(using Context): Unit = try {
168170
tree match {
@@ -226,6 +228,13 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
226228
throw ex
227229
}
228230

231+
/**Reused EqHashSet, safe to use as each TypeDependencyTraverser is used atomically
232+
* Avoid cycles by remembering both the types (testcase:
233+
* tests/run/enum-values.scala) and the symbols of named types (testcase:
234+
* tests/pos-java-interop/i13575) we've seen before.
235+
*/
236+
private val scratchSeen = new util.EqHashSet[Symbol | Type](128)
237+
229238
/** Traverse a used type and record all the dependencies we need to keep track
230239
* of for incremental recompilation.
231240
*
@@ -262,17 +271,13 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
262271
private abstract class TypeDependencyTraverser(using Context) extends TypeTraverser() {
263272
protected def addDependency(symbol: Symbol): Unit
264273

265-
// Avoid cycles by remembering both the types (testcase:
266-
// tests/run/enum-values.scala) and the symbols of named types (testcase:
267-
// tests/pos-java-interop/i13575) we've seen before.
268-
val seen = new mutable.HashSet[Symbol | Type]
269-
def traverse(tp: Type): Unit = if (!seen.contains(tp)) {
270-
seen += tp
274+
scratchSeen.clear(resetToInitial = false)
275+
276+
def traverse(tp: Type): Unit = if scratchSeen.add(tp) then {
271277
tp match {
272278
case tp: NamedType =>
273279
val sym = tp.symbol
274-
if !seen.contains(sym) && !sym.is(Package) then
275-
seen += sym
280+
if !sym.is(Package) && scratchSeen.add(sym) then
276281
addDependency(sym)
277282
if !sym.isClass then traverse(tp.info)
278283
traverse(tp.prefix)
@@ -306,8 +311,6 @@ private class ExtractDependenciesCollector(rec: DependencyRecorder) extends tpd.
306311
}
307312
}
308313

309-
case class ClassDependency(fromClass: Symbol, toClass: Symbol, context: DependencyContext)
310-
311314
/** Record dependencies using `addUsedName`/`addClassDependency` and inform Zinc using `sendToZinc()`.
312315
*
313316
* Note: As an alternative design choice, we could directly call the appropriate
@@ -319,10 +322,10 @@ case class ClassDependency(fromClass: Symbol, toClass: Symbol, context: Dependen
319322
class DependencyRecorder {
320323
import ExtractDependencies.*
321324

322-
/** A map from a non-local class to the names it uses, this does not include
325+
/** A map from a non-local class to the names and classes it uses, this does not include
323326
* names which are only defined and not referenced.
324327
*/
325-
def usedNames: collection.Map[Symbol, UsedNamesInClass] = _usedNames
328+
def foundDeps: util.ReadOnlyMap[Symbol, FoundDepsInClass] = _foundDeps
326329

327330
/** Record a reference to the name of `sym` from the current non-local
328331
* enclosing class.
@@ -355,10 +358,9 @@ class DependencyRecorder {
355358
* safely.
356359
*/
357360
def addUsedRawName(name: Name, includeSealedChildren: Boolean = false)(using Context): Unit = {
358-
val fromClass = resolveDependencySource
361+
val fromClass = resolveDependencyFromClass
359362
if (fromClass.exists) {
360-
val usedName = _usedNames.getOrElseUpdate(fromClass, new UsedNamesInClass)
361-
usedName.update(name, includeSealedChildren)
363+
lastFoundCache.recordName(name, includeSealedChildren)
362364
}
363365
}
364366

@@ -367,24 +369,34 @@ class DependencyRecorder {
367369
private val DefaultScopes = EnumSet.of(UseScope.Default)
368370
private val PatMatScopes = EnumSet.of(UseScope.Default, UseScope.PatMatTarget)
369371

370-
/** An object that maintain the set of used names from within a class */
371-
final class UsedNamesInClass {
372+
/** An object that maintain the set of used names and class dependencies from within a class */
373+
final class FoundDepsInClass {
372374
/** Each key corresponds to a name used in the class. To understand the meaning
373375
* of the associated value, see the documentation of parameter `includeSealedChildren`
374376
* of `addUsedRawName`.
375377
*/
376-
private val _names = new mutable.HashMap[Name, DefaultScopes.type | PatMatScopes.type]
378+
private val _names = new util.HashMap[Name, DefaultScopes.type | PatMatScopes.type]
379+
380+
/** Each key corresponds to a class dependency used in the class.
381+
*/
382+
private val _classes = util.EqHashMap[Symbol, EnumSet[DependencyContext]]()
383+
384+
def addDependency(fromClass: Symbol, context: DependencyContext): Unit =
385+
val set = _classes.getOrElseUpdate(fromClass, EnumSet.noneOf(classOf[DependencyContext]))
386+
set.add(context)
387+
388+
def classes: Iterator[(Symbol, EnumSet[DependencyContext])] = _classes.iterator
377389

378-
def names: collection.Map[Name, EnumSet[UseScope]] = _names
390+
def names: Iterator[(Name, EnumSet[UseScope])] = _names.iterator
379391

380-
private[DependencyRecorder] def update(name: Name, includeSealedChildren: Boolean): Unit = {
392+
private[DependencyRecorder] def recordName(name: Name, includeSealedChildren: Boolean): Unit = {
381393
if (includeSealedChildren)
382394
_names(name) = PatMatScopes
383395
else
384396
_names.getOrElseUpdate(name, DefaultScopes)
385397
}
386398

387-
override def toString(): String = {
399+
def namesString: String = {
388400
val builder = new StringBuilder
389401
names.foreach { case (name, scopes) =>
390402
builder.append(name.mangledString)
@@ -395,51 +407,59 @@ class DependencyRecorder {
395407
}
396408
builder.toString()
397409
}
398-
}
399-
400-
401-
private val _classDependencies = new mutable.HashSet[ClassDependency]
402410

403-
def classDependencies: Set[ClassDependency] = _classDependencies
411+
def classesString: String = {
412+
val builder = new StringBuilder
413+
classes.foreach { case (clazz, scopes) =>
414+
builder.append(clazz.toString)
415+
builder.append(" in [")
416+
scopes.forEach(scope => builder.append(scope.toString))
417+
builder.append("]")
418+
builder.append(", ")
419+
}
420+
builder.toString()
421+
}
422+
}
404423

405424
/** Record a dependency to the class `to` in a given `context`
406425
* from the current non-local enclosing class.
407426
*/
408427
def addClassDependency(toClass: Symbol, context: DependencyContext)(using Context): Unit =
409-
val fromClass = resolveDependencySource
428+
val fromClass = resolveDependencyFromClass
410429
if (fromClass.exists)
411-
_classDependencies += ClassDependency(fromClass, toClass, context)
430+
lastFoundCache.addDependency(toClass, context)
412431

413-
private val _usedNames = new mutable.HashMap[Symbol, UsedNamesInClass]
432+
private val _foundDeps = new util.EqHashMap[Symbol, FoundDepsInClass]
414433

415434
/** Send the collected dependency information to Zinc and clear the local caches. */
416435
def sendToZinc()(using Context): Unit =
417436
ctx.withIncCallback: cb =>
418-
usedNames.foreach:
419-
case (clazz, usedNames) =>
420-
val className = classNameAsString(clazz)
421-
usedNames.names.foreach:
422-
case (usedName, scopes) =>
423-
cb.usedName(className, usedName.toString, scopes)
424437
val siblingClassfiles = new mutable.HashMap[PlainFile, Path]
425-
classDependencies.foreach(recordClassDependency(cb, _, siblingClassfiles))
438+
_foundDeps.iterator.foreach:
439+
case (clazz, foundDeps) =>
440+
val className = classNameAsString(clazz)
441+
foundDeps.names.foreach: (usedName, scopes) =>
442+
cb.usedName(className, usedName.toString, scopes)
443+
for (toClass, deps) <- foundDeps.classes do
444+
for dep <- deps.asScala do
445+
recordClassDependency(cb, clazz, toClass, dep, siblingClassfiles)
426446
clear()
427447

428448
/** Clear all state. */
429449
def clear(): Unit =
430-
_usedNames.clear()
431-
_classDependencies.clear()
450+
_foundDeps.clear()
432451
lastOwner = NoSymbol
433452
lastDepSource = NoSymbol
453+
lastFoundCache = null
434454
_responsibleForImports = NoSymbol
435455

436456
/** Handles dependency on given symbol by trying to figure out if represents a term
437457
* that is coming from either source code (not necessarily compiled in this compilation
438458
* run) or from class file and calls respective callback method.
439459
*/
440-
private def recordClassDependency(cb: interfaces.IncrementalCallback, dep: ClassDependency,
441-
siblingClassfiles: mutable.Map[PlainFile, Path])(using Context): Unit = {
442-
val fromClassName = classNameAsString(dep.fromClass)
460+
private def recordClassDependency(cb: interfaces.IncrementalCallback, fromClass: Symbol, toClass: Symbol,
461+
depCtx: DependencyContext, siblingClassfiles: mutable.Map[PlainFile, Path])(using Context): Unit = {
462+
val fromClassName = classNameAsString(fromClass)
443463
val sourceFile = ctx.compilationUnit.source
444464

445465
/**For a `.tasty` file, constructs a sibling class to the `jpath`.
@@ -465,13 +485,13 @@ class DependencyRecorder {
465485
})
466486

467487
def binaryDependency(path: Path, binaryClassName: String) =
468-
cb.binaryDependency(path, binaryClassName, fromClassName, sourceFile, dep.context)
488+
cb.binaryDependency(path, binaryClassName, fromClassName, sourceFile, depCtx)
469489

470-
val depClass = dep.toClass
490+
val depClass = toClass
471491
val depFile = depClass.associatedFile
472492
if depFile != null then {
473493
// Cannot ignore inheritance relationship coming from the same source (see sbt/zinc#417)
474-
def allowLocal = dep.context == DependencyByInheritance || dep.context == LocalDependencyByInheritance
494+
def allowLocal = depCtx == DependencyByInheritance || depCtx == LocalDependencyByInheritance
475495
val isTasty = depFile.hasTastyExtension
476496

477497
def processExternalDependency() = {
@@ -485,7 +505,7 @@ class DependencyRecorder {
485505
case pf: PlainFile => // The dependency comes from a class file, Zinc handles JRT filesystem
486506
binaryDependency(if isTasty then cachedSiblingClass(pf) else pf.jpath, binaryClassName)
487507
case _ =>
488-
internalError(s"Ignoring dependency $depFile of unknown class ${depFile.getClass}}", dep.fromClass.srcPos)
508+
internalError(s"Ignoring dependency $depFile of unknown class ${depFile.getClass}}", fromClass.srcPos)
489509
}
490510
}
491511

@@ -495,23 +515,28 @@ class DependencyRecorder {
495515
// We cannot ignore dependencies coming from the same source file because
496516
// the dependency info needs to propagate. See source-dependencies/trait-trait-211.
497517
val toClassName = classNameAsString(depClass)
498-
cb.classDependency(toClassName, fromClassName, dep.context)
518+
cb.classDependency(toClassName, fromClassName, depCtx)
499519
}
500520
}
501521

502522
private var lastOwner: Symbol = _
503523
private var lastDepSource: Symbol = _
524+
private var lastFoundCache: FoundDepsInClass | Null = _
504525

505526
/** The source of the dependency according to `nonLocalEnclosingClass`
506527
* if it exists, otherwise fall back to `responsibleForImports`.
507528
*
508529
* This is backed by a cache which is invalidated when `ctx.owner` changes.
509530
*/
510-
private def resolveDependencySource(using Context): Symbol = {
531+
private def resolveDependencyFromClass(using Context): Symbol = {
532+
import dotty.tools.uncheckedNN
511533
if (lastOwner != ctx.owner) {
512534
lastOwner = ctx.owner
513535
val source = nonLocalEnclosingClass
514-
lastDepSource = if (source.is(PackageClass)) responsibleForImports else source
536+
val fromClass = if (source.is(PackageClass)) responsibleForImports else source
537+
if lastDepSource != fromClass then
538+
lastDepSource = fromClass
539+
lastFoundCache = _foundDeps.getOrElseUpdate(fromClass, new FoundDepsInClass)
515540
}
516541

517542
lastDepSource

compiler/src/dotty/tools/dotc/typer/Synthesizer.scala

-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import annotation.{tailrec, constructorOnly}
2020
import ast.tpd._
2121
import Synthesizer._
2222
import sbt.ExtractDependencies.*
23-
import sbt.ClassDependency
2423
import xsbti.api.DependencyContext._
2524

2625
/** Synthesize terms for special classes */

compiler/src/dotty/tools/dotc/util/EqHashMap.scala

+16
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,22 @@ extends GenericHashMap[Key, Value](initialCapacity, capacityMultiple):
5858
used += 1
5959
if used > limit then growTable()
6060

61+
override def getOrElseUpdate(key: Key, value: => Value): Value =
62+
// created by blending lookup and update, avoid having to recompute hash and probe
63+
Stats.record(statsItem("lookup-or-update"))
64+
var idx = firstIndex(key)
65+
var k = keyAt(idx)
66+
while k != null do
67+
if isEqual(k, key) then return valueAt(idx)
68+
idx = nextIndex(idx)
69+
k = keyAt(idx)
70+
val v = value
71+
setKey(idx, key)
72+
setValue(idx, v)
73+
used += 1
74+
if used > limit then growTable()
75+
v
76+
6177
private def addOld(key: Key, value: Value): Unit =
6278
Stats.record(statsItem("re-enter"))
6379
var idx = firstIndex(key)

0 commit comments

Comments
 (0)