From e9303d9757a963ad73140bbb303b64e5c69d1a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pap=20L=C5=91rinc?= Date: Tue, 15 Nov 2016 17:13:41 +0200 Subject: [PATCH 1/3] Updated benchmark dependencies --- test/benchmarks/README.md | 7 ++----- test/benchmarks/build.sbt | 4 ++-- test/benchmarks/project/plugins.sbt | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/test/benchmarks/README.md b/test/benchmarks/README.md index 370d610bc4ab..6c77b8360595 100644 --- a/test/benchmarks/README.md +++ b/test/benchmarks/README.md @@ -5,9 +5,7 @@ that makes use of the [SBT plugin](https://github.com/ktoso/sbt-jmh) for [JMH](h ## Running a benchmark -The benchmarks require first building Scala into `../../build/pack` with `ant`. -If you want to build with `sbt dist/mkPack` instead, -you'll need to change `scalaHome` in this project. +The benchmarks require first building Scala into `../../build/pack`. You'll then need to know the fully-qualified name of the benchmark runner class. The benchmarking classes are organized under `src/main/scala`, @@ -18,8 +16,7 @@ Using this example, one would simply run jmh:runMain scala.collection.mutable.OpenHashMapRunner -in SBT. -SBT should be run _from this directory_. +in SBT, run _from this directory_ (`test/benchmarks`). The JMH results can be found under `target/jmh-results/`. `target` gets deleted on an SBT `clean`, diff --git a/test/benchmarks/build.sbt b/test/benchmarks/build.sbt index fb05fb2c99f1..ef603e18b373 100644 --- a/test/benchmarks/build.sbt +++ b/test/benchmarks/build.sbt @@ -1,5 +1,5 @@ scalaHome := Some(file("../../build/pack")) -scalaVersion := "2.12.0-dev" +scalaVersion := "2.12.1-dev" scalacOptions ++= Seq("-feature", "-opt:l:classpath") lazy val root = (project in file(".")). @@ -7,5 +7,5 @@ lazy val root = (project in file(".")). settings( name := "test-benchmarks", version := "0.0.1", - libraryDependencies += "org.openjdk.jol" % "jol-core" % "0.4" + libraryDependencies += "org.openjdk.jol" % "jol-core" % "0.6" ) diff --git a/test/benchmarks/project/plugins.sbt b/test/benchmarks/project/plugins.sbt index aa49ad98722a..1b79ce888c14 100644 --- a/test/benchmarks/project/plugins.sbt +++ b/test/benchmarks/project/plugins.sbt @@ -1,2 +1,2 @@ addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") -addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.16") +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.17") From 5c93cd2431276fe3c712cb60e8a7a696c1776f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pap=20L=C5=91rinc?= Date: Tue, 15 Nov 2016 17:14:35 +0200 Subject: [PATCH 2/3] Added benchmarks for Vector and HashMap --- .../immutable/VectorMapBenchmark.scala | 32 +++++++++ .../collection/mutable/HashMapBenchmark.scala | 70 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 test/benchmarks/src/main/scala/scala/collection/immutable/VectorMapBenchmark.scala create mode 100644 test/benchmarks/src/main/scala/scala/collection/mutable/HashMapBenchmark.scala diff --git a/test/benchmarks/src/main/scala/scala/collection/immutable/VectorMapBenchmark.scala b/test/benchmarks/src/main/scala/scala/collection/immutable/VectorMapBenchmark.scala new file mode 100644 index 000000000000..61e621dcdffd --- /dev/null +++ b/test/benchmarks/src/main/scala/scala/collection/immutable/VectorMapBenchmark.scala @@ -0,0 +1,32 @@ +package scala.collection.immutable + +import org.openjdk.jmh.annotations._ +import org.openjdk.jmh.infra._ +import org.openjdk.jmh.runner.IterationType +import benchmark._ +import java.util.concurrent.TimeUnit + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class VectorMapBenchmark { + @Param(Array("10", "100", "1000")) + var size: Int = _ + + var values: Vector[Any] = _ + + @Setup(Level.Trial) def initKeys(): Unit = { + values = (0 to size).map(i => (i % 4) match { + case 0 => i.toString + case 1 => i.toChar + case 2 => i.toDouble + case 3 => i.toInt + }).toVector + } + + @Benchmark def groupBy = values.groupBy(_.getClass) +} diff --git a/test/benchmarks/src/main/scala/scala/collection/mutable/HashMapBenchmark.scala b/test/benchmarks/src/main/scala/scala/collection/mutable/HashMapBenchmark.scala new file mode 100644 index 000000000000..3f01d154e934 --- /dev/null +++ b/test/benchmarks/src/main/scala/scala/collection/mutable/HashMapBenchmark.scala @@ -0,0 +1,70 @@ +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 scala.collection.mutable + +@BenchmarkMode(Array(Mode.AverageTime)) +@Fork(2) +@Threads(1) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +class HashMapBenchmark { + @Param(Array("10", "100", "1000")) + var size: Int = _ + + var existingKeys: Array[Any] = _ + var missingKeys: Array[Any] = _ + + @Setup(Level.Trial) def initKeys(): Unit = { + existingKeys = (0 to size).map(i => (i % 4) match { + case 0 => i.toString + case 1 => i.toChar + case 2 => i.toDouble + case 3 => i.toInt + }).toArray + missingKeys = (size to 2 * size).toArray + } + + var map = new mutable.HashMap[Any, Any] + + @Setup(Level.Invocation) def initializeMutable = existingKeys.foreach(v => map.put(v, v)) + + @TearDown(Level.Invocation) def tearDown = map.clear() + + @Benchmark def getOrElseUpdate(bh: Blackhole): Unit = { + var i = 0; + while (i < size) { + bh.consume(map.getOrElseUpdate(existingKeys(i), -1)) + bh.consume(map.getOrElseUpdate(missingKeys(i), -1)) + i += 1 + } + } + + @Benchmark def get(bh: Blackhole): Unit = { + var i = 0; + while (i < size) { + bh.consume(map.get(existingKeys(i), -1)) + bh.consume(map.get(missingKeys(i), -1)) + i += 1 + } + } + + @Benchmark def put(bh: Blackhole): Any = { + var map = new mutable.HashMap[Any, Any] + + var i = 0; + while (i < size) { + map.put(existingKeys(i), i) + i += 1 + } + + map + } +} From b67ca7dc6bb84758f9c9f64d68b0b11c20995aa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pap=20L=C5=91rinc?= Date: Wed, 16 Nov 2016 17:27:36 +0200 Subject: [PATCH 3/3] Changed HashMap.getOrElseUpdate to only calculate the index once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://issues.scala-lang.org/browse/SI-10049 Since `groupBy` uses this method extensively and suffered a measurable slowdown in `2.12.0`, this modification restores (and exceeds) its original speed. --- included benchmarks: (`ns/op` → smaller is better) `before (2.12.0):` ```java Benchmark (size) Mode Cnt Score Error Units s.c.immutable.VectorMapBenchmark.groupBy 10 avgt 20 865.693 ± 7.869 ns/op s.c.immutable.VectorMapBenchmark.groupBy 100 avgt 20 3095.657 ± 56.438 ns/op s.c.immutable.VectorMapBenchmark.groupBy 1000 avgt 20 28247.005 ± 470.513 ns/op s.c.mutable.HashMapBenchmark.get 10 avgt 20 679.448 ± 11.809 ns/op s.c.mutable.HashMapBenchmark.get 100 avgt 20 7240.178 ± 61.734 ns/op s.c.mutable.HashMapBenchmark.get 1000 avgt 20 95725.127 ± 2373.458 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 10 avgt 20 836.561 ± 20.085 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 100 avgt 20 7891.368 ± 56.808 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 1000 avgt 20 97478.629 ± 1782.497 ns/op s.c.mutable.HashMapBenchmark.put 10 avgt 20 243.422 ± 2.915 ns/op s.c.mutable.HashMapBenchmark.put 100 avgt 20 5810.927 ± 60.054 ns/op s.c.mutable.HashMapBenchmark.put 1000 avgt 20 82175.539 ± 1690.296 ns/op ``` `after:` ```java Benchmark (size) Mode Cnt Score Error Units s.c.immutable.VectorMapBenchmark.groupBy 10 avgt 20 627.007 ± 9.718 ns/op s.c.immutable.VectorMapBenchmark.groupBy 100 avgt 20 2086.955 ± 19.042 ns/op s.c.immutable.VectorMapBenchmark.groupBy 1000 avgt 20 19515.234 ± 173.647 ns/op s.c.mutable.HashMapBenchmark.get 10 avgt 20 683.977 ± 11.843 ns/op s.c.mutable.HashMapBenchmark.get 100 avgt 20 7345.675 ± 41.092 ns/op s.c.mutable.HashMapBenchmark.get 1000 avgt 20 95085.926 ± 1702.997 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 10 avgt 20 503.208 ± 2.643 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 100 avgt 20 5526.483 ± 28.262 ns/op s.c.mutable.HashMapBenchmark.getOrElseUpdate 1000 avgt 20 69265.900 ± 674.958 ns/op s.c.mutable.HashMapBenchmark.put 10 avgt 20 252.481 ± 7.597 ns/op s.c.mutable.HashMapBenchmark.put 100 avgt 20 5708.034 ± 110.360 ns/op s.c.mutable.HashMapBenchmark.put 1000 avgt 20 82051.378 ± 1432.009 ns/op ``` i.e. for the given benchmark conditions `~40%` faster `groupBy` and `getOrElseUpdate` --- .../scala/collection/mutable/HashMap.scala | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/library/scala/collection/mutable/HashMap.scala b/src/library/scala/collection/mutable/HashMap.scala index eab4202353a5..11ff1f089353 100644 --- a/src/library/scala/collection/mutable/HashMap.scala +++ b/src/library/scala/collection/mutable/HashMap.scala @@ -72,6 +72,37 @@ extends AbstractMap[A, B] else Some(e.value) } + override def getOrElseUpdate(key: A, defaultValue: => B): B = { + val i = index(elemHashCode(key)) + val entry = findEntry(key, i) + if (entry != null) entry.value + else addEntry(createNewEntry(key, defaultValue), i) + } + + /* inlined HashTable.findEntry0 to preserve its visibility */ + private[this] def findEntry(key: A, h: Int): Entry = { + var e = table(h).asInstanceOf[Entry] + while (notFound(key, e)) + e = e.next + e + } + private[this] def notFound(key: A, e: Entry): Boolean = (e != null) && !elemEquals(e.key, key) + + /* inlined HashTable.addEntry0 to preserve its visibility */ + private[this] def addEntry(e: Entry, h: Int): B = { + if (tableSize >= threshold) addEntry(e) + else addEntry0(e, h) + e.value + } + + /* extracted to make addEntry inlinable */ + private[this] def addEntry0(e: Entry, h: Int) { + e.next = table(h).asInstanceOf[Entry] + table(h) = e + tableSize += 1 + nnSizeMapAdd(h) + } + override def put(key: A, value: B): Option[B] = { val e = findOrAddEntry(key, value) if (e eq null) None