Skip to content

Commit d20c624

Browse files
committed
add EqHashSet
1 parent b18b744 commit d20c624

File tree

4 files changed

+343
-67
lines changed

4 files changed

+343
-67
lines changed
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package dotty.tools.dotc.util
2+
3+
import dotty.tools.uncheckedNN
4+
5+
object EqHashSet:
6+
7+
def from[T](xs: IterableOnce[T]): EqHashSet[T] =
8+
val set = new EqHashSet[T]()
9+
set ++= xs
10+
set
11+
12+
/** A hash set that allows some privileged protected access to its internals
13+
* @param initialCapacity Indicates the initial number of slots in the hash table.
14+
* The actual number of slots is always a power of 2, so the
15+
* initial size of the table will be the smallest power of two
16+
* that is equal or greater than the given `initialCapacity`.
17+
* Minimum value is 4.
18+
* @param capacityMultiple The minimum multiple of capacity relative to used elements.
19+
* The hash table will be re-sized once the number of elements
20+
* multiplied by capacityMultiple exceeds the current size of the hash table.
21+
* However, a table of size up to DenseLimit will be re-sized only
22+
* once the number of elements reaches the table's size.
23+
*/
24+
class EqHashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends GenericHashSet[T](initialCapacity, capacityMultiple) {
25+
import GenericHashSet.DenseLimit
26+
27+
/** System's identity hashcode left shifted by 1 */
28+
final def hash(key: T): Int =
29+
System.identityHashCode(key) << 1
30+
31+
/** reference equality */
32+
final def isEqual(x: T, y: T): Boolean = x.asInstanceOf[AnyRef] eq y.asInstanceOf[AnyRef]
33+
34+
/** Turn hashcode `x` into a table index */
35+
private def index(x: Int): Int = x & (table.length - 1)
36+
37+
private def firstIndex(x: T) = if isDense then 0 else index(hash(x))
38+
private def nextIndex(idx: Int) =
39+
Stats.record(statsItem("miss"))
40+
index(idx + 1)
41+
42+
private def entryAt(idx: Int): T | Null = table(idx).asInstanceOf[T | Null]
43+
private def setEntry(idx: Int, x: T) = table(idx) = x.asInstanceOf[AnyRef | Null]
44+
45+
override def lookup(x: T): T | Null =
46+
Stats.record(statsItem("lookup"))
47+
var idx = firstIndex(x)
48+
var e: T | Null = entryAt(idx)
49+
while e != null do
50+
if isEqual(e.uncheckedNN, x) then return e
51+
idx = nextIndex(idx)
52+
e = entryAt(idx)
53+
null
54+
55+
/** Add entry at `x` at index `idx` */
56+
private def addEntryAt(idx: Int, x: T): T =
57+
Stats.record(statsItem("addEntryAt"))
58+
setEntry(idx, x)
59+
used += 1
60+
if used > limit then growTable()
61+
x
62+
63+
/** attempts to put `x` in the Set, if it was not entered before, return true, else return false. */
64+
override def add(x: T): Boolean =
65+
Stats.record(statsItem("enter"))
66+
var idx = firstIndex(x)
67+
var e: T | Null = entryAt(idx)
68+
while e != null do
69+
if isEqual(e.uncheckedNN, x) then return false // already entered
70+
idx = nextIndex(idx)
71+
e = entryAt(idx)
72+
addEntryAt(idx, x)
73+
true // first entry
74+
75+
override def put(x: T): T =
76+
Stats.record(statsItem("put"))
77+
var idx = firstIndex(x)
78+
var e: T | Null = entryAt(idx)
79+
while e != null do
80+
// TODO: remove uncheckedNN when explicit-nulls is enabled for regule compiling
81+
if isEqual(e.uncheckedNN, x) then return e.uncheckedNN
82+
idx = nextIndex(idx)
83+
e = entryAt(idx)
84+
addEntryAt(idx, x)
85+
86+
override def +=(x: T): Unit = put(x)
87+
88+
override def remove(x: T): Boolean =
89+
Stats.record(statsItem("remove"))
90+
var idx = firstIndex(x)
91+
var e: T | Null = entryAt(idx)
92+
while e != null do
93+
if isEqual(e.uncheckedNN, x) then
94+
var hole = idx
95+
while
96+
idx = nextIndex(idx)
97+
e = entryAt(idx)
98+
e != null
99+
do
100+
val eidx = index(hash(e.uncheckedNN))
101+
if isDense
102+
|| index(eidx - (hole + 1)) > index(idx - (hole + 1))
103+
// entry `e` at `idx` can move unless `index(hash(e))` is in
104+
// the (ring-)interval [hole + 1 .. idx]
105+
then
106+
setEntry(hole, e.uncheckedNN)
107+
hole = idx
108+
table(hole) = null
109+
used -= 1
110+
return true
111+
idx = nextIndex(idx)
112+
e = entryAt(idx)
113+
false
114+
115+
override def -=(x: T): Unit =
116+
remove(x)
117+
118+
private def addOld(x: T) =
119+
Stats.record(statsItem("re-enter"))
120+
var idx = firstIndex(x)
121+
var e = entryAt(idx)
122+
while e != null do
123+
idx = nextIndex(idx)
124+
e = entryAt(idx)
125+
setEntry(idx, x)
126+
127+
override def copyFrom(oldTable: Array[AnyRef | Null]): Unit =
128+
if isDense then
129+
Array.copy(oldTable, 0, table, 0, oldTable.length)
130+
else
131+
var idx = 0
132+
while idx < oldTable.length do
133+
val e: T | Null = oldTable(idx).asInstanceOf[T | Null]
134+
if e != null then addOld(e.uncheckedNN)
135+
idx += 1
136+
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package dotty.tools.dotc.util
2+
3+
import dotty.tools.uncheckedNN
4+
5+
object GenericHashSet:
6+
7+
/** The number of elements up to which dense packing is used.
8+
* If the number of elements reaches `DenseLimit` a hash table is used instead
9+
*/
10+
inline val DenseLimit = 8
11+
12+
/** A hash set that allows some privileged protected access to its internals
13+
* @param initialCapacity Indicates the initial number of slots in the hash table.
14+
* The actual number of slots is always a power of 2, so the
15+
* initial size of the table will be the smallest power of two
16+
* that is equal or greater than the given `initialCapacity`.
17+
* Minimum value is 4.
18+
* @param capacityMultiple The minimum multiple of capacity relative to used elements.
19+
* The hash table will be re-sized once the number of elements
20+
* multiplied by capacityMultiple exceeds the current size of the hash table.
21+
* However, a table of size up to DenseLimit will be re-sized only
22+
* once the number of elements reaches the table's size.
23+
*/
24+
abstract class GenericHashSet[T](initialCapacity: Int = 8, capacityMultiple: Int = 2) extends MutableSet[T] {
25+
import GenericHashSet.DenseLimit
26+
27+
protected var used: Int = _
28+
protected var limit: Int = _
29+
protected var table: Array[AnyRef | Null] = _
30+
31+
clear()
32+
33+
private def allocate(capacity: Int) =
34+
table = new Array[AnyRef | Null](capacity)
35+
limit = if capacity <= DenseLimit then capacity - 1 else capacity / capacityMultiple
36+
37+
private def roundToPower(n: Int) =
38+
if n < 4 then 4
39+
else if Integer.bitCount(n) == 1 then n
40+
else 1 << (32 - Integer.numberOfLeadingZeros(n))
41+
42+
def clear(resetToInitial: Boolean): Unit =
43+
used = 0
44+
if resetToInitial then allocate(roundToPower(initialCapacity))
45+
else java.util.Arrays.fill(table, null)
46+
47+
/** The number of elements in the set */
48+
def size: Int = used
49+
50+
protected def isDense = limit < DenseLimit
51+
52+
/** Hashcode, by default a processed `x.hashCode`, can be overridden */
53+
protected def hash(key: T): Int
54+
55+
/** Hashcode, by default `equals`, can be overridden */
56+
protected def isEqual(x: T, y: T): Boolean
57+
58+
/** Turn hashcode `x` into a table index */
59+
private def index(x: Int): Int = x & (table.length - 1)
60+
61+
protected def currentTable: Array[AnyRef | Null] = table
62+
63+
private def firstIndex(x: T) = if isDense then 0 else index(hash(x))
64+
private def nextIndex(idx: Int) =
65+
Stats.record(statsItem("miss"))
66+
index(idx + 1)
67+
68+
private def entryAt(idx: Int): T | Null = table(idx).asInstanceOf[T | Null]
69+
private def setEntry(idx: Int, x: T) = table(idx) = x.asInstanceOf[AnyRef | Null]
70+
71+
def lookup(x: T): T | Null =
72+
Stats.record(statsItem("lookup"))
73+
var idx = firstIndex(x)
74+
var e: T | Null = entryAt(idx)
75+
while e != null do
76+
if isEqual(e.uncheckedNN, x) then return e
77+
idx = nextIndex(idx)
78+
e = entryAt(idx)
79+
null
80+
81+
/** Add entry at `x` at index `idx` */
82+
private def addEntryAt(idx: Int, x: T): T =
83+
Stats.record(statsItem("addEntryAt"))
84+
setEntry(idx, x)
85+
used += 1
86+
if used > limit then growTable()
87+
x
88+
89+
/** attempts to put `x` in the Set, if it was not entered before, return true, else return false. */
90+
override def add(x: T): Boolean =
91+
Stats.record(statsItem("enter"))
92+
var idx = firstIndex(x)
93+
var e: T | Null = entryAt(idx)
94+
while e != null do
95+
if isEqual(e.uncheckedNN, x) then return false // already entered
96+
idx = nextIndex(idx)
97+
e = entryAt(idx)
98+
addEntryAt(idx, x)
99+
true // first entry
100+
101+
def put(x: T): T =
102+
Stats.record(statsItem("put"))
103+
var idx = firstIndex(x)
104+
var e: T | Null = entryAt(idx)
105+
while e != null do
106+
// TODO: remove uncheckedNN when explicit-nulls is enabled for regule compiling
107+
if isEqual(e.uncheckedNN, x) then return e.uncheckedNN
108+
idx = nextIndex(idx)
109+
e = entryAt(idx)
110+
addEntryAt(idx, x)
111+
112+
def +=(x: T): Unit = put(x)
113+
114+
def remove(x: T): Boolean =
115+
Stats.record(statsItem("remove"))
116+
var idx = firstIndex(x)
117+
var e: T | Null = entryAt(idx)
118+
while e != null do
119+
if isEqual(e.uncheckedNN, x) then
120+
var hole = idx
121+
while
122+
idx = nextIndex(idx)
123+
e = entryAt(idx)
124+
e != null
125+
do
126+
val eidx = index(hash(e.uncheckedNN))
127+
if isDense
128+
|| index(eidx - (hole + 1)) > index(idx - (hole + 1))
129+
// entry `e` at `idx` can move unless `index(hash(e))` is in
130+
// the (ring-)interval [hole + 1 .. idx]
131+
then
132+
setEntry(hole, e.uncheckedNN)
133+
hole = idx
134+
table(hole) = null
135+
used -= 1
136+
return true
137+
idx = nextIndex(idx)
138+
e = entryAt(idx)
139+
false
140+
141+
def -=(x: T): Unit =
142+
remove(x)
143+
144+
private def addOld(x: T) =
145+
Stats.record(statsItem("re-enter"))
146+
var idx = firstIndex(x)
147+
var e = entryAt(idx)
148+
while e != null do
149+
idx = nextIndex(idx)
150+
e = entryAt(idx)
151+
setEntry(idx, x)
152+
153+
def copyFrom(oldTable: Array[AnyRef | Null]): Unit =
154+
if isDense then
155+
Array.copy(oldTable, 0, table, 0, oldTable.length)
156+
else
157+
var idx = 0
158+
while idx < oldTable.length do
159+
val e: T | Null = oldTable(idx).asInstanceOf[T | Null]
160+
if e != null then addOld(e.uncheckedNN)
161+
idx += 1
162+
163+
protected def growTable(): Unit =
164+
val oldTable = table
165+
val newLength =
166+
if oldTable.length == DenseLimit then DenseLimit * 2 * roundToPower(capacityMultiple)
167+
else table.length * 2
168+
allocate(newLength)
169+
copyFrom(oldTable)
170+
171+
abstract class EntryIterator extends Iterator[T]:
172+
def entry(idx: Int): T | Null
173+
private var idx = 0
174+
def hasNext =
175+
while idx < table.length && table(idx) == null do idx += 1
176+
idx < table.length
177+
def next() =
178+
require(hasNext)
179+
try entry(idx).uncheckedNN finally idx += 1
180+
181+
def iterator: Iterator[T] = new EntryIterator():
182+
def entry(idx: Int) = entryAt(idx)
183+
184+
override def toString: String =
185+
iterator.mkString("HashSet(", ", ", ")")
186+
187+
protected def statsItem(op: String) =
188+
val prefix = if isDense then "HashSet(dense)." else "HashSet."
189+
val suffix = getClass.getSimpleName
190+
s"$prefix$op $suffix"
191+
}

0 commit comments

Comments
 (0)