From 7b5d1788c58b155b281ff7884a877d2be99b7205 Mon Sep 17 00:00:00 2001 From: Liang Yan Date: Sun, 13 Nov 2022 21:40:08 +0800 Subject: [PATCH] new LinkedHashMap/LinkedHashSet implementation The mutable HashMap/HashSet has been rewroten and the performance is better. So rewrote the LinkedHashMap and LinkedHashSet also to improve performance. The detailed data can be seen in the PR. Most codes are same with HashMap/HashSet but some are different: 1. To keep binary compatibility, only api in old solution are updated.The two new class LinkedHashMap/LinkedHashSet still don't have parameters. hashcode can't be realized since it needs a new iterator which will break binary compatibility. 2. specific method to handle the order when adding/removing the nodes. 3. other minor changes. Signed-off-by: Liang Yan --- .../collection/mutable/LinkedHashMap.scala | 382 ++++++++++++++---- .../collection/mutable/LinkedHashSet.scala | 216 ++++++++-- .../mutable/LinkedHashMapBenchmark2.scala | 216 ++++++++++ 3 files changed, 689 insertions(+), 125 deletions(-) create mode 100644 test/benchmarks/src/main/scala/scala/collection/mutable/LinkedHashMapBenchmark2.scala diff --git a/src/library/scala/collection/mutable/LinkedHashMap.scala b/src/library/scala/collection/mutable/LinkedHashMap.scala index 22e9b4c53a1..ddc039ef6bd 100644 --- a/src/library/scala/collection/mutable/LinkedHashMap.scala +++ b/src/library/scala/collection/mutable/LinkedHashMap.scala @@ -14,35 +14,9 @@ package scala package collection package mutable -import scala.annotation.nowarn +import scala.annotation.{nowarn, tailrec} import scala.collection.generic.DefaultSerializable -/** $factoryInfo - * @define Coll `LinkedHashMap` - * @define coll linked hash map - */ -@SerialVersionUID(3L) -object LinkedHashMap extends MapFactory[LinkedHashMap] { - - def empty[K, V] = new LinkedHashMap[K, V] - - def from[K, V](it: collection.IterableOnce[(K, V)]) = - it match { - case lhm: LinkedHashMap[K, V] => lhm - case _ => Growable.from(empty[K, V], it) - } - - def newBuilder[K, V] = new GrowableBuilder(empty[K, V]) - - /** Class for the linked hash map entry, used internally. - */ - private[mutable] final class LinkedEntry[K, V](val key: K, var value: V) - extends HashEntry[K, LinkedEntry[K, V]] { - var earlier: LinkedEntry[K, V] = null - var later: LinkedEntry[K, V] = null - } - -} /** This class implements mutable maps using a hashtable. * The iterator and all traversal methods of this class visit elements in the order they were inserted. @@ -70,36 +44,19 @@ class LinkedHashMap[K, V] // stepper / keyStepper / valueStepper are not overridden to use XTableStepper because that stepper // would not return the elements in insertion order - private[collection] type Entry = LinkedHashMap.LinkedEntry[K, V] private[collection] def _firstEntry: Entry = firstEntry @transient protected var firstEntry: Entry = null + @transient protected var lastEntry: Entry = null - @transient private[this] var table: HashTable[K, V, Entry] = newHashTable - // Used by scala-java8-compat (private[mutable] erases to public, so Java code can access it) - private[mutable] def getTable: HashTable[K, V, Entry] = table + @transient private[this] var table = new Array[Entry](tableSizeFor(LinkedHashMap.defaultinitialSize)) - private def newHashTable = - new HashTable[K, V, Entry] { - def createNewEntry(key: K, value: V): Entry = { - val e = new Entry(key, value) - if (firstEntry eq null) firstEntry = e - else { lastEntry.later = e; e.earlier = lastEntry } - lastEntry = e - e - } + private[this] var threshold: Int = newThreshold(table.length) + private[this] def newThreshold(size: Int) = (size.toDouble * LinkedHashMap.defaultLoadFactor).toInt - override def foreachEntry[U](f: Entry => U): Unit = { - var cur = firstEntry - while (cur ne null) { - f(cur) - cur = cur.later - } - } - - } + private[this] var contentSize = 0 override def last: (K, V) = if (size > 0) (lastEntry.key, lastEntry.value) @@ -117,47 +74,93 @@ class LinkedHashMap[K, V] if (size > 0) Some((firstEntry.key, firstEntry.value)) else None - override def size = table.tableSize + override def size = contentSize override def knownSize: Int = size - override def isEmpty: Boolean = table.tableSize == 0 + override def isEmpty: Boolean = size == 0 def get(key: K): Option[V] = { - val e = table.findEntry(key) + val e = findEntry(key) if (e == null) None else Some(e.value) } override def contains(key: K): Boolean = { if (getClass eq classOf[LinkedHashMap[_, _]]) - table.findEntry(key) != null + findEntry(key) != null else super.contains(key) // A subclass might override `get`, use the default implementation `contains`. } override def put(key: K, value: V): Option[V] = { - val e = table.findOrAddEntry(key, value) - if (e eq null) None - else { val v = e.value; e.value = value; Some(v) } + put0(key, value, true) match { + case null => None + case sm => sm + } } override def update(key: K, value: V): Unit = { - val e = table.findOrAddEntry(key, value) - if (e ne null) e.value = value + put0(key, value, false) + } override def remove(key: K): Option[V] = { - val e = table.removeEntry(key) - if (e eq null) None - else Some(remove0(e)) + removeEntry0(key) match { + case null => None + case nd => Some(nd.value) + } } - private[this] def remove0(e: Entry): V = { - if (e.earlier eq null) firstEntry = e.later - else e.earlier.later = e.later - if (e.later eq null) lastEntry = e.earlier - else e.later.earlier = e.earlier - e.earlier = null // Null references to prevent nepotism - e.later = null - e.value + private[this] def removeEntry0(elem: K): Entry = removeEntry0(elem, computeHash(elem)) + + /** Removes a key from this map if it exists + * + * @param elem the element to remove + * @param hash the **improved** hashcode of `element` (see computeHash) + * @return the node that contained element if it was present, otherwise null + */ + private[this] def removeEntry0(elem: K, hash: Int): Entry = { + val idx = index(hash) + table(idx) match { + case null => null + case nd if nd.hash == hash && nd.key == elem => + // first element matches + table(idx) = nd.next + deleteEntry(nd) + contentSize -= 1 + nd + case nd => + // find an element that matches + var prev = nd + var next = nd.next + while ((next ne null) && next.hash <= hash) { + if (next.hash == hash && next.key == elem) { + prev.next = next.next + deleteEntry(next) + contentSize -= 1 + return next + } + prev = next + next = next.next + } + null + } + } + + /** Computes the improved hash of an original (`any.##`) hash. */ + @`inline` private[this] def improveHash(originalHash: Int): Int = { + originalHash ^ (originalHash >>> 16) + } + + /** Computes the improved hash of this key */ + @`inline` private[this] def computeHash(o: K): Int = improveHash(o.##) + + @`inline` private[this] def index(hash: Int) = hash & (table.length - 1) + + @`inline` private[this] def findEntry(key: K): Entry = { + val hash = computeHash(key) + table(index(hash)) match { + case null => null + case nd => nd.findEntry(key, hash) + } } def addOne(kv: (K, V)): this.type = { put(kv._1, kv._2); this } @@ -189,28 +192,53 @@ class LinkedHashMap[K, V] // Override updateWith for performance, so we can do the update while hashing // the input key only once and performing one lookup into the hash table override def updateWith(key: K)(remappingFunction: Option[V] => Option[V]): Option[V] = { - val keyIndex = table.index(table.elemHashCode(key)) - val entry = table.findEntry0(key, keyIndex) + val hash = computeHash(key) + val indexedHash = index(hash) + + var foundEntry = null.asInstanceOf[Entry] + var previousEntry = null.asInstanceOf[Entry] + table(indexedHash) match { + case null => + case nd => + @tailrec + def findEntry(prev: Entry, nd: Entry, k: K, h: Int): Unit = { + if (h == nd.hash && k == nd.key) { + previousEntry = prev + foundEntry = nd + } + else if ((nd.next eq null) || (nd.hash > h)) () + else findEntry(nd, nd.next, k, h) + } - val previousValue = - if (entry == null) None - else Some(entry.value) + findEntry(null, nd, key, hash) + } + + val previousValue = foundEntry match { + case null => None + case nd => Some(nd.value) + } val nextValue = remappingFunction(previousValue) (previousValue, nextValue) match { case (None, None) => // do nothing + case (Some(_), None) => - remove0(entry) - table.removeEntry0(key, keyIndex) + if (previousEntry != null) previousEntry.next = foundEntry.next + else table(indexedHash) = foundEntry.next + deleteEntry(foundEntry) + contentSize -= 1 case (None, Some(value)) => - table.addEntry0(table.createNewEntry(key, value), keyIndex) - - case (Some(_), Some(value)) => - entry.value = value + val newIndexedHash = + if (contentSize + 1 >= threshold) { + growTable(table.length * 2) + index(hash) + } else indexedHash + put0(key, value, false, hash, newIndexedHash) + + case (Some(_), Some(newValue)) => foundEntry.value = newValue } - nextValue } @@ -239,26 +267,210 @@ class LinkedHashMap[K, V] } override def clear(): Unit = { - table.clearTable() + java.util.Arrays.fill(table.asInstanceOf[Array[AnyRef]], null) + contentSize = 0 firstEntry = null lastEntry = null } + private[this] def tableSizeFor(capacity: Int) = + (Integer.highestOneBit((capacity-1).max(4))*2).min(1 << 30) + + /*create a new entry. If table is empty(firstEntry is null), then the + * new entry will be the firstEntry. If not, just set the new entry to + * be the lastEntry. + * */ + private[this] def createNewEntry(key: K, hash: Int, value: V): Entry = + { + val e = new Entry(key, hash, value) + if (firstEntry eq null) firstEntry = e + else { lastEntry.later = e; e.earlier = lastEntry } + lastEntry = e + e + } + + /*delete the entry from the linkedhashmap. set its earlier entry's later entry + * and later entry's earlier entry correctly.and set its earlier and later to + * be null.*/ + private[this] def deleteEntry(e: Entry): Unit = { + if (e.earlier eq null) firstEntry = e.later + else e.earlier.later = e.later + if (e.later eq null) lastEntry = e.earlier + else e.later.earlier = e.earlier + e.earlier = null // Null references to prevent nepotism + e.later = null + } + + private[this] def put0(key: K, value: V, getOld: Boolean): Some[V] = { + if(contentSize + 1 >= threshold) growTable(table.length * 2) + val hash = computeHash(key) + val idx = index(hash) + put0(key, value, getOld, hash, idx) + } + + private[this] def put0(key: K, value: V, getOld: Boolean, hash: Int, idx: Int): Some[V] = { + table(idx) match { + case null => + val nnode = createNewEntry(key, hash, value) + nnode.next = null + table(idx) = nnode + case old => + var prev = null.asInstanceOf[Entry] + var n = old + while((n ne null) && n.hash <= hash) { + if(n.hash == hash && key == n.key) { + val old = n.value + n.value = value + return if(getOld) Some(old) else null + } + prev = n + n = n.next + } + val nnode = createNewEntry(key, hash, value) + if(prev eq null) { + table(idx) = nnode + nnode.next = old + } + else { + nnode.next = prev.next + prev.next = nnode} + } + contentSize += 1 + null + } + + private[this] def growTable(newlen: Int): Unit = { + if (newlen < 0) + throw new RuntimeException(s"new hash table size $newlen exceeds maximum") + var oldlen = table.length + threshold = newThreshold(newlen) + if (size == 0) table = new Array(newlen) + else { + table = java.util.Arrays.copyOf(table, newlen) + val preLow = new Entry(null.asInstanceOf[K], 0, null.asInstanceOf[V]) + val preHigh = new Entry(null.asInstanceOf[K], 0, null.asInstanceOf[V]) + // Split buckets until the new length has been reached. This could be done more + // efficiently when growing an already filled table to more than double the size. + while (oldlen < newlen) { + var i = 0 + while (i < oldlen) { + val old = table(i) + if (old ne null) { + preLow.next = null + preHigh.next = null + var lastLow = preLow + var lastHigh = preHigh + var n = old + while (n ne null) { + val next = n.next + if ((n.hash & oldlen) == 0) { // keep low + lastLow.next = n + lastLow = n + } else { // move to high + lastHigh.next = n + lastHigh = n + } + n = next + } + lastLow.next = null + if (old ne preLow.next) table(i) = preLow.next + if (preHigh.next ne null) { + table(i + oldlen) = preHigh.next + lastHigh.next = null + } + } + i += 1 + } + oldlen *= 2 + } + } + } + + private[this] def serializeTo(out: java.io.ObjectOutputStream, writeEntry: Entry => Unit): Unit = { + out.writeInt(contentSize) + var cur = firstEntry + while (cur ne null) { + writeEntry(cur) + cur = cur.later + } + } + private def writeObject(out: java.io.ObjectOutputStream): Unit = { out.defaultWriteObject() - table.serializeTo(out, { entry => + serializeTo(out, { entry => out.writeObject(entry.key) out.writeObject(entry.value) }) } + private[this] def serializeFrom(in: java.io.ObjectInputStream, readEntry: => (K, V)): Unit = { + val _contentsize = in.readInt() + assert(_contentsize > 0) + clear() + table = new Array(tableSizeFor(_contentsize)) + threshold = newThreshold(table.length) + + var index = 0 + while (index < size) { + addOne(readEntry) + index += 1 + } + } private def readObject(in: java.io.ObjectInputStream): Unit = { in.defaultReadObject() - table = newHashTable - table.init(in, table.createNewEntry(in.readObject().asInstanceOf[K], in.readObject().asInstanceOf[V])) + serializeFrom(in, (in.readObject().asInstanceOf[K], in.readObject().asInstanceOf[V])) } @nowarn("""cat=deprecation&origin=scala\.collection\.Iterable\.stringPrefix""") override protected[this] def stringPrefix = "LinkedHashMap" } +/** $factoryInfo + * @define Coll `LinkedHashMap` + * @define coll linked hash map + */ +@SerialVersionUID(3L) +object LinkedHashMap extends MapFactory[LinkedHashMap] { + + def empty[K, V] = new LinkedHashMap[K, V] + + def from[K, V](it: collection.IterableOnce[(K, V)]) = + it match { + case lhm: LinkedHashMap[K, V] => lhm + case _ => Growable.from(empty[K, V], it) + } + + def newBuilder[K, V] = new GrowableBuilder(empty[K, V]) + + /** Class for the linked hash map entry, used internally. + */ + private[mutable] final class LinkedEntry[K, V](val key: K, val hash: Int, var value: V) + { + var earlier: LinkedEntry[K, V] = null + var later: LinkedEntry[K, V] = null + var next: LinkedEntry[K, V] = null + + @tailrec + final def findEntry(k: K, h: Int): LinkedEntry[K, V] = + if (h == hash && k == key) this + else if ((next eq null) || (hash > h)) null + else next.findEntry(k, h) + + @tailrec + final def foreach[U](f: ((K, V)) => U): Unit = { + f((key, value)) + if (next ne null) next.foreach(f) + } + + @tailrec + final def foreachEntry[U](f: (K, V) => U): Unit = { + f(key, value) + if (next ne null) next.foreachEntry(f) + } + + } + + private[collection] final def defaultLoadFactor: Double = 0.75 // corresponds to 75% + /** The default initial capacity for the hash table */ + private[collection] final def defaultinitialSize: Int = 16 +} diff --git a/src/library/scala/collection/mutable/LinkedHashSet.scala b/src/library/scala/collection/mutable/LinkedHashSet.scala index 3c190a141dd..959bc9e104f 100644 --- a/src/library/scala/collection/mutable/LinkedHashSet.scala +++ b/src/library/scala/collection/mutable/LinkedHashSet.scala @@ -14,7 +14,7 @@ package scala package collection package mutable -import scala.annotation.nowarn +import scala.annotation.{nowarn, tailrec} import scala.collection.generic.DefaultSerializable /** This class implements mutable sets using a hashtable. @@ -44,29 +44,16 @@ class LinkedHashSet[A] type Entry = LinkedHashSet.Entry[A] @transient protected var firstEntry: Entry = null + @transient protected var lastEntry: Entry = null - @transient private[this] var table: HashTable[A, AnyRef, Entry] = newHashTable - // Used by scala-java8-compat (private[mutable] erases to public, so Java code can access it) - private[mutable] def getTable: HashTable[A, AnyRef, Entry] = table + @transient private[this] var table = new Array[Entry](tableSizeFor(LinkedHashSet.defaultinitialSize)) - private def newHashTable = - new HashTable[A, AnyRef, Entry] { - def createNewEntry(key: A, value: AnyRef) = { - val e = new Entry(key) - if (firstEntry eq null) firstEntry = e - else { lastEntry.later = e; e.earlier = lastEntry } - lastEntry = e - e - } - override def foreachEntry[U](f: Entry => U): Unit = { - var cur = firstEntry - while (cur ne null) { - f(cur) - cur = cur.later - } - } - } + private[this] var threshold: Int = newThreshold(table.length) + + private[this] def newThreshold(size: Int) = (size.toDouble * LinkedHashSet.defaultLoadFactor).toInt + + private[this] var contentSize = 0 override def last: A = if (size > 0) lastEntry.key @@ -84,13 +71,16 @@ class LinkedHashSet[A] if (size > 0) Some(firstEntry.key) else None - override def size: Int = table.tableSize + override def size: Int = contentSize + override def knownSize: Int = size + override def isEmpty: Boolean = size == 0 - def contains(elem: A): Boolean = table.findEntry(elem) ne null + + def contains(elem: A): Boolean = findEntry(elem) ne null def addOne(elem: A): this.type = { - table.findOrAddEntry(elem, null) + addElem(elem, computeHash(elem)) this } @@ -100,17 +90,7 @@ class LinkedHashSet[A] } override def remove(elem: A): Boolean = { - val e = table.removeEntry(elem) - if (e eq null) false - else { - if (e.earlier eq null) firstEntry = e.later - else e.earlier.later = e.later - if (e.later eq null) lastEntry = e.earlier - else e.later.earlier = e.earlier - e.earlier = null // Null references to prevent nepotism - e.later = null - true - } + remove0(elem, computeHash(elem)) } def iterator: Iterator[A] = new AbstractIterator[A] { @@ -130,20 +110,152 @@ class LinkedHashSet[A] } override def clear(): Unit = { - table.clearTable() + java.util.Arrays.fill(table.asInstanceOf[Array[AnyRef]], null) + contentSize = 0 firstEntry = null lastEntry = null } + private[this] def tableSizeFor(capacity: Int) = + (Integer.highestOneBit((capacity-1).max(4))*2).min(1 << 30) + + @`inline` private[this] def improveHash(originalHash: Int): Int = { + originalHash ^ (originalHash >>> 16) + } + + /** Computes the improved hash of this key */ + @`inline` private[this] def computeHash(o: A): Int = improveHash(o.##) + + @`inline` private[this] def index(hash: Int) = hash & (table.length - 1) + + @`inline` private[this] def findEntry(key: A): Entry = { + val hash = computeHash(key) + table(index(hash)) match { + case null => null + case nd => nd.findEntry(key, hash) + } + } + + /*create a new entry. If table is empty(firstEntry is null), then the + * new entry will be the firstEntry. If not, just set the new entry to + * be the lastEntry. + * */ + private[this] def createNewEntry(key: A, hash: Int): Entry = + { + val e = new Entry(key, hash) + if (firstEntry eq null) firstEntry = e + else { lastEntry.later = e; e.earlier = lastEntry } + lastEntry = e + e + } + + /*delete the entry from the linkedhashset. set its earlier entry's later entry + * and later entry's earlier entry correctly.and then set its earlier and later + * to be null.*/ + private[this] def deleteEntry(e: Entry): Unit = + { + if (e.earlier eq null) firstEntry = e.later + else e.earlier.later = e.later + if (e.later eq null) lastEntry = e.earlier + else e.later.earlier = e.earlier + e.earlier = null // Null references to prevent nepotism + e.later = null + } + + /** Adds an element to this set + * @param elem element to add + * @param hash the **improved** hash of `elem` (see computeHash) + */ + private[this] def addElem(elem: A, hash: Int) : Boolean = { + val idx = index(hash) + table(idx) match { + case null => + val nnode = createNewEntry(elem, hash) + table(idx) = nnode + nnode.next = null + case old => + var prev = null.asInstanceOf[Entry] + var n = old + while((n ne null) && n.hash <= hash) { + if(n.hash == hash && elem == n.key) return false + prev = n + n = n.next + } + val nnode = createNewEntry(elem, hash) + if(prev eq null) { + nnode.next = old + table(idx) = nnode + + } else { + nnode.next = prev.next + prev.next = nnode + } + } + contentSize += 1 + true + } + + private[this] def remove0(elem: A, hash: Int): Boolean = { + val idx = index(hash) + table(idx) match { + case null => false + case nd if nd.hash == hash && nd.key == elem => + // first element matches + table(idx) = nd.next + deleteEntry(nd) + nd.next = null + contentSize -= 1 + true + case nd => + // find an element that matches + var prev = nd + var next = nd.next + while((next ne null) && next.hash <= hash) { + if(next.hash == hash && next.key == elem) { + prev.next = next.next + deleteEntry(next) + next.next = null + contentSize -= 1 + return true + } + prev = next + next = next.next + } + false + } + } + + private[this] def serializeTo(out: java.io.ObjectOutputStream, writeEntry: Entry => Unit): Unit = { + out.writeInt(contentSize) + var cur = firstEntry + while (cur ne null) { + writeEntry(cur) + cur = cur.later + } + } + private def writeObject(out: java.io.ObjectOutputStream): Unit = { out.defaultWriteObject() - table.serializeTo(out, { e => out.writeObject(e.key) }) + serializeTo(out, { e => out.writeObject(e.key) }) + } + + private[this] def serializeFrom(in: java.io.ObjectInputStream, readEntry: => A): Unit = { + val _contentsize = in.readInt() + assert(_contentsize > 0) + clear() + table = new Array(tableSizeFor(_contentsize)) + threshold = newThreshold(table.length) + + var index = 0 + while (index < size) { + addOne(readEntry) + index += 1 + } } private def readObject(in: java.io.ObjectInputStream): Unit = { in.defaultReadObject() - table = newHashTable - table.init(in, table.createNewEntry(in.readObject().asInstanceOf[A], null)) + serializeFrom(in, in.readObject().asInstanceOf[A]) } @nowarn("""cat=deprecation&origin=scala\.collection\.Iterable\.stringPrefix""") @@ -169,9 +281,33 @@ object LinkedHashSet extends IterableFactory[LinkedHashSet] { /** Class for the linked hash set entry, used internally. */ - private[mutable] final class Entry[A](val key: A) extends HashEntry[A, Entry[A]] { + private[mutable] final class Entry[A](val key: A, val hash: Int) { var earlier: Entry[A] = null var later: Entry[A] = null + var next: Entry[A] = null + + @tailrec + final def findEntry(k: A, h: Int): Entry[A] = + if (h == hash && k == key) this + else if ((next eq null) || (hash > h)) null + else next.findEntry(k, h) + + @tailrec + final def foreach[U](f: A => U): Unit = { + f(key) + if (next ne null) next.foreach(f) + } + + @tailrec + final def foreachEntry[U](f: A => U): Unit = { + f(key) + if (next ne null) next.foreachEntry(f) + } } + + private[collection] final def defaultLoadFactor: Double = 0.75 // corresponds to 75% + + /** The default initial capacity for the hash table */ + private[collection] final def defaultinitialSize: Int = 16 } diff --git a/test/benchmarks/src/main/scala/scala/collection/mutable/LinkedHashMapBenchmark2.scala b/test/benchmarks/src/main/scala/scala/collection/mutable/LinkedHashMapBenchmark2.scala new file mode 100644 index 00000000000..6725141f3b4 --- /dev/null +++ b/test/benchmarks/src/main/scala/scala/collection/mutable/LinkedHashMapBenchmark2.scala @@ -0,0 +1,216 @@ +package scala.collection.mutable + +import org.openjdk.jmh.annotations._ +import org.openjdk.jmh.infra._ +import org.openjdk.jmh.runner.IterationType +import benchmark._ +import java.util.concurrent.TimeUnit +import java.util.{ LinkedHashMap => JLHashMap, LinkedHashSet => JLHashSet } + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 20) +@Measurement(iterations = 20) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class LinkedHashMapBenchmark2 { + @Param(Array(/*"0", "1",*/ "10", "100", "1000", "10000")) + var size: Int = _ + @Param(Array("true")) + var stringsOnly = false + + class Collider(val x: Any, val h: Int) { + override def hashCode: Int = h + override def equals(o: Any): Boolean = o match { + case o: Collider => x == o.x + case _ => false + } + } + + var existingKeys: Array[Any] = _ + var existingKVs: ArrayBuffer[(Any, Any)] = _ + var missingKeys: Array[Any] = _ + var s1: LinkedHashSet[Any] = _ + var m1: LinkedHashMap[Any, Any] = _ + var j1: JLHashMap[Any, Any] = new JLHashMap[Any, Any] + var j2: JLHashSet[Any] = new JLHashSet[Any] + var colliders: Array[Collider] = _ + + @Setup(Level.Trial) def init: Unit = { + existingKeys = (0 until size).map(i => (i % 4) match { + case _ if stringsOnly => i.toString + case 0 => i.toString + case 1 => i.toChar + case 2 => i.toDouble + case 3 => i.toInt + }).toArray + existingKVs = ArrayBuffer.from(existingKeys.iterator.map(k => (k, k))) + missingKeys = (size until (2 * size.max(100))).toArray.map(_.toString) + s1 = LinkedHashSet.from(existingKeys) + m1 = LinkedHashMap.from(existingKVs) + m1.foreach { case (k, v) => j1.put(k, v) } + s1.foreach({case k => j2.add(k)}) + colliders = existingKeys.map(k => new Collider(k, k.hashCode & 0x1111)) + } + + @Benchmark def lhsFillRegular(bh: Blackhole): Unit = { + val h = new LinkedHashSet[Any] + existingKeys.foreach(k => h.addOne(k)) + bh.consume(h) + } + + @Benchmark def lhsFillColliding(bh: Blackhole): Unit = { + val h = new LinkedHashSet[Any] + colliders.foreach(k => h.addOne(k)) + bh.consume(h) + } + + @Benchmark def lhsBuild(bh: Blackhole): Unit = + bh.consume(LinkedHashSet.from(existingKeys)) + + @Benchmark def lhsIterate(bh: Blackhole): Unit = { + val it = s1.iterator + while(it.hasNext) bh.consume(it.next()) + } + + @Benchmark def lhsContainsTrue(bh: Blackhole): Unit = { + var i = 0 + while (i < size) { + bh.consume(s1.contains(existingKeys(i))) + i += 1 + } + } + + @Benchmark def lhsContainsFalse(bh: Blackhole): Unit = { + var i = 0 + while (i < size.max(100)) { + bh.consume(s1.contains(missingKeys(i))) + i += 1 + } + } + + @Benchmark def lhmFillRegular(bh: Blackhole): Unit = { + val h = new LinkedHashMap[Any, Any] + existingKeys.foreach(k => h.put(k, k)) + bh.consume(h) + } + + @Benchmark def lhmFillColliding(bh: Blackhole): Unit = { + val h = new LinkedHashMap[Any, Any] + colliders.foreach(k => h.put(k, k)) + bh.consume(h) + } + + @Benchmark def lhmBuild(bh: Blackhole): Unit = + bh.consume(LinkedHashMap.from(existingKVs)) + + @Benchmark def lhmIterateKeys(bh: Blackhole): Unit = { + val it = m1.keysIterator + while(it.hasNext) bh.consume(it.next()) + } + + @Benchmark def lhmIterateEntries(bh: Blackhole): Unit = { + val it = m1.iterator + while(it.hasNext) bh.consume(it.next()) + } + + @Benchmark def lhmGetExisting(bh: Blackhole): Unit = { + var i = 0 + while (i < size) { + bh.consume(m1.apply(existingKeys(i))) + i += 1 + } + } + + @Benchmark def lhmGetNone(bh: Blackhole): Unit = { + var i = 0 + while (i < size.max(100)) { + bh.consume(m1.get(missingKeys(i))) + i += 1 + } + } + + @Benchmark def javalhmFillRegular(bh: Blackhole): Unit = { + val h = new JLHashMap[Any, Any] + existingKeys.foreach(k => h.put(k, k)) + bh.consume(h) + } + + @Benchmark def javalhmFillColliding(bh: Blackhole): Unit = { + val h = new JLHashMap[Any, Any] + colliders.foreach(k => h.put(k, k)) + bh.consume(h) + } + + @Benchmark def javalhmBuild(bh: Blackhole): Unit = { + val h = new JLHashMap[Any, Any](((existingKeys.length+1).toDouble/0.75).toInt, 0.75f) + existingKeys.foreach(k => h.put(k, k)) + bh.consume(h) + } + + @Benchmark def javalhmIterateKeys(bh: Blackhole): Unit = { + val it = j1.keySet().iterator() + while(it.hasNext) bh.consume(it.next()) + } + + @Benchmark def javalhmIterateEntries(bh: Blackhole): Unit = { + val it = j1.entrySet().iterator() + while(it.hasNext) bh.consume(it.next()) + } + + @Benchmark def javalhmGetExisting(bh: Blackhole): Unit = { + var i = 0 + while (i < size) { + bh.consume(j1.get(existingKeys(i))) + i += 1 + } + } + + @Benchmark def javalhmGetNone(bh: Blackhole): Unit = { + var i = 0 + while (i < size.max(100)) { + bh.consume(j1.get(missingKeys(i))) + i += 1 + } + } + @Benchmark def javalhsFillRegular(bh: Blackhole): Unit = { + val h = new JLHashSet[Any] + existingKeys.foreach(k => h.add(k)) + bh.consume(h) + } + + @Benchmark def javalhsFillColliding(bh: Blackhole): Unit = { + val h = new JLHashSet[Any] + colliders.foreach(k => h.add(k)) + bh.consume(h) + } + + @Benchmark def javalhsBuild(bh: Blackhole): Unit = { + val h = new JLHashSet[Any](((existingKeys.length+1).toDouble/0.75).toInt, 0.75f) + existingKeys.foreach(k => h.add(k)) + bh.consume(h) + } + + @Benchmark def javalhsIterate(bh: Blackhole): Unit = { + val it = j2.iterator() + while(it.hasNext) bh.consume(it.next()) + } + + + @Benchmark def javalhsContainsTrue(bh: Blackhole): Unit = { + var i = 0 + while (i < size) { + bh.consume(j2.contains(existingKeys(i))) + i += 1 + } + } + + @Benchmark def javalhsContainsFalse(bh: Blackhole): Unit = { + var i = 0 + while (i < size.max(100)) { + bh.consume(j2.contains(missingKeys(i))) + i += 1 + } + } +}