From a454a990cc1a760b129376f69392359f03e58d2f Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 25 Aug 2020 10:33:46 +0200 Subject: [PATCH 1/3] Cache all memberNamed results Use a full cache instead of an LRU cache. For `typer/*.scala`, this reduced computed member searches on normal ClassDenotations from 520K to 170K. Cache hit rate improves to 97.5%, from 92.5%. (Without member caching there are almost 7Mio computed members for the same code base). --- .../tools/dotc/core/SymDenotations.scala | 12 +-- .../dotty/tools/dotc/util/LinearTable.scala | 89 +++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/util/LinearTable.scala diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 57a90b81695a..2a638b64dca2 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1564,14 +1564,14 @@ object SymDenotations { initPrivateWithin: Symbol) extends SymDenotation(symbol, maybeOwner, name, initFlags, initInfo, initPrivateWithin) { - import util.LRUCache + import util.LinearTable // ----- caches ------------------------------------------------------- private var myTypeParams: List[TypeSymbol] = null private var fullNameCache: SimpleIdentityMap[QualifiedNameKind, Name] = SimpleIdentityMap.Empty - private var myMemberCache: LRUCache[Name, PreDenotation] = null + private var myMemberCache: LinearTable[Name, PreDenotation] = null private var myMemberCachePeriod: Period = Nowhere /** A cache from types T to baseType(T, C) */ @@ -1582,9 +1582,9 @@ object SymDenotations { private var baseDataCache: BaseData = BaseData.None private var memberNamesCache: MemberNames = MemberNames.None - private def memberCache(using Context): LRUCache[Name, PreDenotation] = { + private def memberCache(using Context): LinearTable[Name, PreDenotation] = { if (myMemberCachePeriod != ctx.period) { - myMemberCache = new LRUCache + myMemberCache = LinearTable.empty myMemberCachePeriod = ctx.period } myMemberCache @@ -1868,10 +1868,10 @@ object SymDenotations { final def nonPrivateMembersNamed(name: Name)(using Context): PreDenotation = { Stats.record("nonPrivateMembersNamed") if (Config.cacheMembersNamed) { - var denots: PreDenotation = memberCache lookup name + var denots: PreDenotation = memberCache.lookup(name) if (denots == null) { denots = computeNPMembersNamed(name) - memberCache.enter(name, denots) + myMemberCache = memberCache.enter(name, denots) } else if (Config.checkCacheMembersNamed) { val denots1 = computeNPMembersNamed(name) diff --git a/compiler/src/dotty/tools/dotc/util/LinearTable.scala b/compiler/src/dotty/tools/dotc/util/LinearTable.scala new file mode 100644 index 000000000000..d4bd4db81eda --- /dev/null +++ b/compiler/src/dotty/tools/dotc/util/LinearTable.scala @@ -0,0 +1,89 @@ +package dotty.tools.dotc.util + +/** A table with an immutable API that must be used linearly since it uses + * mutation internally. A packed array representation is used for sizes + * up to 8, and a hash table is used for larger sizes. + */ +abstract class LinearTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef]: + def lookup(key: Key): Value + def enter(key: Key, value: Value): LinearTable[Key, Value] + def invalidate(key: Key): Unit + def size: Int + +object LinearTable: + def empty[Key >: Null <: AnyRef, Value >: Null <: AnyRef]: LinearTable[Key, Value] = + ArrayTable[Key, Value](8) + +class ArrayTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef](capacity: Int) extends LinearTable[Key, Value]: + + val elems = new Array[AnyRef](capacity * 2) + var size = 0 + + def lookup(key: Key): Value = + var i = 0 + while i < elems.length do + if elems(i) eq key then + return elems(i + 1).asInstanceOf[Value] + if elems(i) == null then + return null + i += 2 + null + + def enter(key: Key, value: Value): LinearTable[Key, Value] = + var i = 0 + while i < elems.length do + if elems(i) eq key then + elems(i + 1) = value + return this + if elems(i) == null then + elems(i) = key + elems(i + 1) = value + size += 1 + return this + i += 2 + val ht = HashTable[Key, Value](initialCapacity = 16) + i = 0 + while i < elems.length do + ht.enter(elems(i).asInstanceOf[Key], elems(i + 1).asInstanceOf[Value]) + i += 2 + ht.enter(key, value) + ht + + def invalidate(key: Key): Unit = + var i = 0 + while i < elems.length do + if elems(i) eq key then + size -= 1 + elems(i) = null + return + i += 2 + + override def toString: String = + val buf = new StringBuilder + var i = 0 + while i < elems.length do + buf.append(if i == 0 then "ArrayTable(" else ", ") + if elems(i) != null then + buf.append(elems(i)) + buf.append(" -> ") + buf.append(elems(i + 1)) + i += 2 + buf.append(")") + buf.toString +end ArrayTable + +class HashTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef](initialCapacity: Int) extends LinearTable[Key, Value]: + private val table = java.util.HashMap[Key, Value](initialCapacity) + + def lookup(key: Key): Value = + table.get(key) + def enter(key: Key, value: Value): LinearTable[Key, Value] = + table.put(key, value) + this + def invalidate(key: Key): Unit = + table.remove(key) + def size: Int = + table.size + + override def toString: String = table.toString +end HashTable \ No newline at end of file From bb30c7c9ba2128b50373fba6c73accf5e7db9081 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 25 Aug 2020 15:24:13 +0200 Subject: [PATCH 2/3] Use an optimized HashTable instead of a LinearTable The two implementation classes of LinearTable had so much in common that they could be combined. Since the implementation is now fixed, we can use the usual mutable interface for a table. --- .../tools/dotc/core/SymDenotations.scala | 10 +- .../src/dotty/tools/dotc/util/HashTable.scala | 139 ++++++++++++++++++ .../dotty/tools/dotc/util/LinearTable.scala | 89 ----------- 3 files changed, 144 insertions(+), 94 deletions(-) create mode 100644 compiler/src/dotty/tools/dotc/util/HashTable.scala delete mode 100644 compiler/src/dotty/tools/dotc/util/LinearTable.scala diff --git a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala index 2a638b64dca2..636bd6d18c7f 100644 --- a/compiler/src/dotty/tools/dotc/core/SymDenotations.scala +++ b/compiler/src/dotty/tools/dotc/core/SymDenotations.scala @@ -1564,14 +1564,14 @@ object SymDenotations { initPrivateWithin: Symbol) extends SymDenotation(symbol, maybeOwner, name, initFlags, initInfo, initPrivateWithin) { - import util.LinearTable + import util.HashTable // ----- caches ------------------------------------------------------- private var myTypeParams: List[TypeSymbol] = null private var fullNameCache: SimpleIdentityMap[QualifiedNameKind, Name] = SimpleIdentityMap.Empty - private var myMemberCache: LinearTable[Name, PreDenotation] = null + private var myMemberCache: HashTable[Name, PreDenotation] = null private var myMemberCachePeriod: Period = Nowhere /** A cache from types T to baseType(T, C) */ @@ -1582,9 +1582,9 @@ object SymDenotations { private var baseDataCache: BaseData = BaseData.None private var memberNamesCache: MemberNames = MemberNames.None - private def memberCache(using Context): LinearTable[Name, PreDenotation] = { + private def memberCache(using Context): HashTable[Name, PreDenotation] = { if (myMemberCachePeriod != ctx.period) { - myMemberCache = LinearTable.empty + myMemberCache = HashTable() myMemberCachePeriod = ctx.period } myMemberCache @@ -1871,7 +1871,7 @@ object SymDenotations { var denots: PreDenotation = memberCache.lookup(name) if (denots == null) { denots = computeNPMembersNamed(name) - myMemberCache = memberCache.enter(name, denots) + memberCache.enter(name, denots) } else if (Config.checkCacheMembersNamed) { val denots1 = computeNPMembersNamed(name) diff --git a/compiler/src/dotty/tools/dotc/util/HashTable.scala b/compiler/src/dotty/tools/dotc/util/HashTable.scala new file mode 100644 index 000000000000..86b40872676a --- /dev/null +++ b/compiler/src/dotty/tools/dotc/util/HashTable.scala @@ -0,0 +1,139 @@ +package dotty.tools.dotc.util + +object HashTable: + inline val MaxDense = 8 + +/** A hash table using open hashing with linear scan which is also very space efficient + * at small sizes. + * @param initialCapacity Indicates the initial number of slots in the hash table. + * The actual number of slots is always a power of 2, so the + * initial size of the table will be the smallest power of two + * that is equal or greater than the given `initialCapacity`. + * Minimum value is 4. + * @param loadFactor The maximum fraction of used elements relative to capacity. + * The hash table will be re-sized once the number of elements exceeds + * the current size of the hash table multiplied by loadFactor. + * However, a table of size up to MaxDense will be re-sized to only + * once the number of elements reaches the table's size. + */ +class HashTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef] + (initialCapacity: Int = 8: Int, loadFactor: Float = 0.33f): + import HashTable.MaxDense + private var used: Int = _ + private var limit: Int = _ + private var table: Array[AnyRef] = _ + clear() + + private def allocate(capacity: Int) = + table = new Array[AnyRef](capacity * 2) + limit = if capacity <= MaxDense then capacity - 1 else (capacity * loadFactor).toInt + + private def roundToPower(n: Int) = + if Integer.bitCount(n) == 1 then n + else + def recur(n: Int): Int = + if n == 1 then 2 + else recur(n >>> 1) << 1 + recur(n) + + /** Remove all elements from this table and set back to initial configuration */ + def clear(): Unit = + used = 0 + allocate(roundToPower(initialCapacity max 4)) + + /** The number of elements in the set */ + def size: Int = used + + private def isDense = limit < MaxDense + + /** Hashcode, by default `System.identityHashCode`, but can be overriden */ + protected def hash(x: Key): Int = System.identityHashCode(x) + + /** Equality, by default `eq`, but can be overridden */ + protected def isEqual(x: Key, y: Key): Boolean = x eq y + + /** Turn hashcode `x` into a table index */ + private def index(x: Int): Int = x & (table.length - 2) + + private def firstIndex(key: Key) = if isDense then 0 else index(hash(key)) + private def nextIndex(idx: Int) = index(idx + 2) + + private def keyAt(idx: Int): Key = table(idx).asInstanceOf[Key] + private def valueAt(idx: Int): Value = table(idx + 1).asInstanceOf[Value] + + /** Find entry such that `isEqual(x, entry)`. If it exists, return it. + * If not, enter `x` in set and return `x`. + */ + def lookup(key: Key): Value = + var idx = firstIndex(key) + var k = keyAt(idx) + while k != null do + if isEqual(k, key) then return valueAt(idx) + idx = nextIndex(idx) + k = keyAt(idx) + null + + def enter(key: Key, value: Value): Unit = + var idx = firstIndex(key) + var k = keyAt(idx) + while k != null do + if isEqual(k, key) then + table(idx + 1) = value + return + idx = nextIndex(idx) + k = keyAt(idx) + table(idx) = key + table(idx + 1) = value + used += 1 + if used > limit then growTable() + + def invalidate(key: Key): Unit = + var idx = firstIndex(key) + var k = keyAt(idx) + while k != null do + if isEqual(k, key) then + var hole = idx + if !isDense then + while + idx = nextIndex(idx) + k = keyAt(idx) + k != null && index(hash(k)) != idx + do + table(hole) = k + table(hole + 1) = valueAt(idx) + hole = idx + table(hole) = null + used -= 1 + return + idx = nextIndex(idx) + k = keyAt(idx) + + private def addOld(key: Key, value: AnyRef): Unit = + var idx = firstIndex(key) + var k = keyAt(idx) + while k != null do + idx = nextIndex(idx) + k = keyAt(idx) + table(idx) = key + table(idx + 1) = value + + private def growTable(): Unit = + val oldTable = table + allocate(table.length) + if isDense then + Array.copy(oldTable, 0, table, 0, oldTable.length) + else + var idx = 0 + while idx < oldTable.length do + val key = oldTable(idx).asInstanceOf[Key] + if key != null then addOld(key, oldTable(idx + 1)) + idx += 2 + + def iterator: Iterator[(Key, Value)] = + for idx <- (0 until table.length by 2).iterator + if keyAt(idx) != null + yield (keyAt(idx), valueAt(idx)) + + override def toString: String = + iterator.map((k, v) => s"$k -> $v").mkString("LinearTable(", ", ", ")") +end HashTable diff --git a/compiler/src/dotty/tools/dotc/util/LinearTable.scala b/compiler/src/dotty/tools/dotc/util/LinearTable.scala deleted file mode 100644 index d4bd4db81eda..000000000000 --- a/compiler/src/dotty/tools/dotc/util/LinearTable.scala +++ /dev/null @@ -1,89 +0,0 @@ -package dotty.tools.dotc.util - -/** A table with an immutable API that must be used linearly since it uses - * mutation internally. A packed array representation is used for sizes - * up to 8, and a hash table is used for larger sizes. - */ -abstract class LinearTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef]: - def lookup(key: Key): Value - def enter(key: Key, value: Value): LinearTable[Key, Value] - def invalidate(key: Key): Unit - def size: Int - -object LinearTable: - def empty[Key >: Null <: AnyRef, Value >: Null <: AnyRef]: LinearTable[Key, Value] = - ArrayTable[Key, Value](8) - -class ArrayTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef](capacity: Int) extends LinearTable[Key, Value]: - - val elems = new Array[AnyRef](capacity * 2) - var size = 0 - - def lookup(key: Key): Value = - var i = 0 - while i < elems.length do - if elems(i) eq key then - return elems(i + 1).asInstanceOf[Value] - if elems(i) == null then - return null - i += 2 - null - - def enter(key: Key, value: Value): LinearTable[Key, Value] = - var i = 0 - while i < elems.length do - if elems(i) eq key then - elems(i + 1) = value - return this - if elems(i) == null then - elems(i) = key - elems(i + 1) = value - size += 1 - return this - i += 2 - val ht = HashTable[Key, Value](initialCapacity = 16) - i = 0 - while i < elems.length do - ht.enter(elems(i).asInstanceOf[Key], elems(i + 1).asInstanceOf[Value]) - i += 2 - ht.enter(key, value) - ht - - def invalidate(key: Key): Unit = - var i = 0 - while i < elems.length do - if elems(i) eq key then - size -= 1 - elems(i) = null - return - i += 2 - - override def toString: String = - val buf = new StringBuilder - var i = 0 - while i < elems.length do - buf.append(if i == 0 then "ArrayTable(" else ", ") - if elems(i) != null then - buf.append(elems(i)) - buf.append(" -> ") - buf.append(elems(i + 1)) - i += 2 - buf.append(")") - buf.toString -end ArrayTable - -class HashTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef](initialCapacity: Int) extends LinearTable[Key, Value]: - private val table = java.util.HashMap[Key, Value](initialCapacity) - - def lookup(key: Key): Value = - table.get(key) - def enter(key: Key, value: Value): LinearTable[Key, Value] = - table.put(key, value) - this - def invalidate(key: Key): Unit = - table.remove(key) - def size: Int = - table.size - - override def toString: String = table.toString -end HashTable \ No newline at end of file From 53961f31879e6aec238c0784c37667ee73229259 Mon Sep 17 00:00:00 2001 From: Martin Odersky Date: Tue, 25 Aug 2020 18:01:20 +0200 Subject: [PATCH 3/3] Improve re-allocation of hash tables When going from dense to hashed, we should expand by a multiple greater than two, or we will have to reallocate immediately afterwards. --- .../src/dotty/tools/dotc/util/HashTable.scala | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/HashTable.scala b/compiler/src/dotty/tools/dotc/util/HashTable.scala index 86b40872676a..42c65f53154e 100644 --- a/compiler/src/dotty/tools/dotc/util/HashTable.scala +++ b/compiler/src/dotty/tools/dotc/util/HashTable.scala @@ -1,7 +1,10 @@ package dotty.tools.dotc.util object HashTable: - inline val MaxDense = 8 + /** The number of elements up to which dense packing is used. + * If the number of elements reaches `DenseLimit` a hash table is used instead + */ + inline val DenseLimit = 8 /** A hash table using open hashing with linear scan which is also very space efficient * at small sizes. @@ -10,15 +13,16 @@ object HashTable: * initial size of the table will be the smallest power of two * that is equal or greater than the given `initialCapacity`. * Minimum value is 4. - * @param loadFactor The maximum fraction of used elements relative to capacity. - * The hash table will be re-sized once the number of elements exceeds - * the current size of the hash table multiplied by loadFactor. - * However, a table of size up to MaxDense will be re-sized to only + * @param capacityMultiple The minimum multiple of capacity relative to used elements. + * The hash table will be re-sized once the number of elements + * multiplied by capacityMultiple exceeds the current size of the hash table. + * However, a table of size up to DenseLimit will be re-sized only * once the number of elements reaches the table's size. */ class HashTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef] - (initialCapacity: Int = 8: Int, loadFactor: Float = 0.33f): - import HashTable.MaxDense + (initialCapacity: Int = 8, capacityMultiple: Int = 3): + import HashTable.DenseLimit + private var used: Int = _ private var limit: Int = _ private var table: Array[AnyRef] = _ @@ -26,15 +30,11 @@ class HashTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef] private def allocate(capacity: Int) = table = new Array[AnyRef](capacity * 2) - limit = if capacity <= MaxDense then capacity - 1 else (capacity * loadFactor).toInt + limit = if capacity <= DenseLimit then capacity - 1 else capacity / capacityMultiple private def roundToPower(n: Int) = if Integer.bitCount(n) == 1 then n - else - def recur(n: Int): Int = - if n == 1 then 2 - else recur(n >>> 1) << 1 - recur(n) + else 1 << (32 - Integer.numberOfLeadingZeros(n)) /** Remove all elements from this table and set back to initial configuration */ def clear(): Unit = @@ -44,7 +44,7 @@ class HashTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef] /** The number of elements in the set */ def size: Int = used - private def isDense = limit < MaxDense + private def isDense = limit < DenseLimit /** Hashcode, by default `System.identityHashCode`, but can be overriden */ protected def hash(x: Key): Int = System.identityHashCode(x) @@ -119,7 +119,10 @@ class HashTable[Key >: Null <: AnyRef, Value >: Null <: AnyRef] private def growTable(): Unit = val oldTable = table - allocate(table.length) + val newLength = + if oldTable.length == DenseLimit then DenseLimit * 2 * roundToPower(capacityMultiple) + else table.length + allocate(newLength) if isDense then Array.copy(oldTable, 0, table, 0, oldTable.length) else