Skip to content

Commit 9c5d3f8

Browse files
authored
Merge pull request scala#5528 from paplorinc/getOrElseUpdate
Changed HashMap.getOrElseUpdate to only calculate the index once
2 parents 8cdd46f + b67ca7d commit 9c5d3f8

File tree

6 files changed

+138
-8
lines changed

6 files changed

+138
-8
lines changed

src/library/scala/collection/mutable/HashMap.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,37 @@ extends AbstractMap[A, B]
7272
else Some(e.value)
7373
}
7474

75+
override def getOrElseUpdate(key: A, defaultValue: => B): B = {
76+
val i = index(elemHashCode(key))
77+
val entry = findEntry(key, i)
78+
if (entry != null) entry.value
79+
else addEntry(createNewEntry(key, defaultValue), i)
80+
}
81+
82+
/* inlined HashTable.findEntry0 to preserve its visibility */
83+
private[this] def findEntry(key: A, h: Int): Entry = {
84+
var e = table(h).asInstanceOf[Entry]
85+
while (notFound(key, e))
86+
e = e.next
87+
e
88+
}
89+
private[this] def notFound(key: A, e: Entry): Boolean = (e != null) && !elemEquals(e.key, key)
90+
91+
/* inlined HashTable.addEntry0 to preserve its visibility */
92+
private[this] def addEntry(e: Entry, h: Int): B = {
93+
if (tableSize >= threshold) addEntry(e)
94+
else addEntry0(e, h)
95+
e.value
96+
}
97+
98+
/* extracted to make addEntry inlinable */
99+
private[this] def addEntry0(e: Entry, h: Int) {
100+
e.next = table(h).asInstanceOf[Entry]
101+
table(h) = e
102+
tableSize += 1
103+
nnSizeMapAdd(h)
104+
}
105+
75106
override def put(key: A, value: B): Option[B] = {
76107
val e = findOrAddEntry(key, value)
77108
if (e eq null) None

test/benchmarks/README.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ that makes use of the [SBT plugin](https://github.com/ktoso/sbt-jmh) for [JMH](h
55

66
## Running a benchmark
77

8-
The benchmarks require first building Scala into `../../build/pack` with `ant`.
9-
If you want to build with `sbt dist/mkPack` instead,
10-
you'll need to change `scalaHome` in this project.
8+
The benchmarks require first building Scala into `../../build/pack`.
119

1210
You'll then need to know the fully-qualified name of the benchmark runner class.
1311
The benchmarking classes are organized under `src/main/scala`,
@@ -18,8 +16,7 @@ Using this example, one would simply run
1816

1917
jmh:runMain scala.collection.mutable.OpenHashMapRunner
2018

21-
in SBT.
22-
SBT should be run _from this directory_.
19+
in SBT, run _from this directory_ (`test/benchmarks`).
2320

2421
The JMH results can be found under `target/jmh-results/`.
2522
`target` gets deleted on an SBT `clean`,

test/benchmarks/build.sbt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
scalaHome := Some(file("../../build/pack"))
2-
scalaVersion := "2.12.0-dev"
2+
scalaVersion := "2.12.1-dev"
33
scalacOptions ++= Seq("-feature", "-opt:l:classpath")
44

55
lazy val root = (project in file(".")).
66
enablePlugins(JmhPlugin).
77
settings(
88
name := "test-benchmarks",
99
version := "0.0.1",
10-
libraryDependencies += "org.openjdk.jol" % "jol-core" % "0.4"
10+
libraryDependencies += "org.openjdk.jol" % "jol-core" % "0.6"
1111
)

test/benchmarks/project/plugins.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0")
2-
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.16")
2+
addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.17")
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package scala.collection.immutable
2+
3+
import org.openjdk.jmh.annotations._
4+
import org.openjdk.jmh.infra._
5+
import org.openjdk.jmh.runner.IterationType
6+
import benchmark._
7+
import java.util.concurrent.TimeUnit
8+
9+
@BenchmarkMode(Array(Mode.AverageTime))
10+
@Fork(2)
11+
@Threads(1)
12+
@Warmup(iterations = 10)
13+
@Measurement(iterations = 10)
14+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
15+
@State(Scope.Benchmark)
16+
class VectorMapBenchmark {
17+
@Param(Array("10", "100", "1000"))
18+
var size: Int = _
19+
20+
var values: Vector[Any] = _
21+
22+
@Setup(Level.Trial) def initKeys(): Unit = {
23+
values = (0 to size).map(i => (i % 4) match {
24+
case 0 => i.toString
25+
case 1 => i.toChar
26+
case 2 => i.toDouble
27+
case 3 => i.toInt
28+
}).toVector
29+
}
30+
31+
@Benchmark def groupBy = values.groupBy(_.getClass)
32+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package scala.collection.mutable
2+
3+
import org.openjdk.jmh.annotations._
4+
import org.openjdk.jmh.infra._
5+
import org.openjdk.jmh.runner.IterationType
6+
import benchmark._
7+
import java.util.concurrent.TimeUnit
8+
9+
import scala.collection.mutable
10+
11+
@BenchmarkMode(Array(Mode.AverageTime))
12+
@Fork(2)
13+
@Threads(1)
14+
@Warmup(iterations = 10)
15+
@Measurement(iterations = 10)
16+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
17+
@State(Scope.Benchmark)
18+
class HashMapBenchmark {
19+
@Param(Array("10", "100", "1000"))
20+
var size: Int = _
21+
22+
var existingKeys: Array[Any] = _
23+
var missingKeys: Array[Any] = _
24+
25+
@Setup(Level.Trial) def initKeys(): Unit = {
26+
existingKeys = (0 to size).map(i => (i % 4) match {
27+
case 0 => i.toString
28+
case 1 => i.toChar
29+
case 2 => i.toDouble
30+
case 3 => i.toInt
31+
}).toArray
32+
missingKeys = (size to 2 * size).toArray
33+
}
34+
35+
var map = new mutable.HashMap[Any, Any]
36+
37+
@Setup(Level.Invocation) def initializeMutable = existingKeys.foreach(v => map.put(v, v))
38+
39+
@TearDown(Level.Invocation) def tearDown = map.clear()
40+
41+
@Benchmark def getOrElseUpdate(bh: Blackhole): Unit = {
42+
var i = 0;
43+
while (i < size) {
44+
bh.consume(map.getOrElseUpdate(existingKeys(i), -1))
45+
bh.consume(map.getOrElseUpdate(missingKeys(i), -1))
46+
i += 1
47+
}
48+
}
49+
50+
@Benchmark def get(bh: Blackhole): Unit = {
51+
var i = 0;
52+
while (i < size) {
53+
bh.consume(map.get(existingKeys(i), -1))
54+
bh.consume(map.get(missingKeys(i), -1))
55+
i += 1
56+
}
57+
}
58+
59+
@Benchmark def put(bh: Blackhole): Any = {
60+
var map = new mutable.HashMap[Any, Any]
61+
62+
var i = 0;
63+
while (i < size) {
64+
map.put(existingKeys(i), i)
65+
i += 1
66+
}
67+
68+
map
69+
}
70+
}

0 commit comments

Comments
 (0)