From 2b5fb0b68ac7b3b0711c283b45a8452218a7d38f Mon Sep 17 00:00:00 2001 From: Julien Richard-Foy Date: Wed, 18 Jan 2017 10:56:24 +0100 Subject: [PATCH] Add micro-benchmarks --- README.md | 18 ++++- .../strawman/collection/MemoryFootprint.scala | 50 +++++++++++++ .../immutable/LazyListBenchmark.scala | 58 +++++++++++++++ .../collection/immutable/ListBenchmark.scala | 70 +++++++++++++++++++ .../immutable/ScalaListBenchmark.scala | 58 +++++++++++++++ .../mutable/ArrayBufferBenchmark.scala | 59 ++++++++++++++++ .../mutable/ListBufferBenchmark.scala | 58 +++++++++++++++ build.sbt | 34 ++++++--- project/plugins.sbt | 1 + .../scala/strawman/collection/Iterable.scala | 4 ++ src/main/scala/strawman/collection/View.scala | 14 ++++ .../collection/immutable/LazyList.scala | 1 + .../strawman/collection/immutable/List.scala | 2 + 13 files changed, 415 insertions(+), 12 deletions(-) create mode 100644 benchmarks/memory/src/main/scala/strawman/collection/MemoryFootprint.scala create mode 100644 benchmarks/time/src/main/scala/strawman/collection/immutable/LazyListBenchmark.scala create mode 100644 benchmarks/time/src/main/scala/strawman/collection/immutable/ListBenchmark.scala create mode 100644 benchmarks/time/src/main/scala/strawman/collection/immutable/ScalaListBenchmark.scala create mode 100644 benchmarks/time/src/main/scala/strawman/collection/mutable/ArrayBufferBenchmark.scala create mode 100644 benchmarks/time/src/main/scala/strawman/collection/mutable/ListBufferBenchmark.scala create mode 100644 project/plugins.sbt diff --git a/README.md b/README.md index 38eca03111..285e69bf6b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -## Collection-Strawman +# Collection-Strawman [![Join the chat at https://gitter.im/scala/collection-strawman](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/scala/collection-strawman?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) @@ -7,3 +7,19 @@ Prototype improvements for Scala collections. - [Gitter Discussion](https://gitter.im/scala/collection-strawman) - [Dotty Issue](https://github.com/lampepfl/dotty/issues/818) - [Scala Center Proposal](https://github.com/scalacenter/advisoryboard/blob/master/proposals/007-collections.md) + +## Build + +- Compile the collections and run the + tests: + ~~~ + >; compile; test + ~~~ +- Run the memory benchmark: + ~~~ + > memoryBenchmark/run + ~~~ +- Run the execution time benchmark: + ~~~ + > timeBenchmark/jmh:run + ~~~ diff --git a/benchmarks/memory/src/main/scala/strawman/collection/MemoryFootprint.scala b/benchmarks/memory/src/main/scala/strawman/collection/MemoryFootprint.scala new file mode 100644 index 0000000000..0cff56447b --- /dev/null +++ b/benchmarks/memory/src/main/scala/strawman/collection/MemoryFootprint.scala @@ -0,0 +1,50 @@ +package bench + +import strawman.collection.immutable.{LazyList, List} + +import scala.{Any, AnyRef, App, Int, Long} +import scala.Predef.{println, ArrowAssoc} +import scala.compat.Platform +import java.lang.Runtime + +import strawman.collection.mutable.{ArrayBuffer, ListBuffer} + +object MemoryFootprint extends App { + + val sizes = scala.List(8, 64, 512, 4096, 32768, 262144, 2097152) + + val runtime = Runtime.getRuntime + val obj: AnyRef = null + var placeholder: Any = _ + + def benchmark[A](gen: Int => A): scala.List[(Int, Long)] = ( + // We run 5 iterations and pick the last result only + for (_ <- scala.Range(0, 5)) yield { + for (size <- sizes) yield { + placeholder = null + Platform.collectGarbage() + val memBefore = runtime.totalMemory() - runtime.freeMemory() + placeholder = gen(size) + Platform.collectGarbage() + val memAfter = runtime.totalMemory() - runtime.freeMemory() + size -> (memAfter - memBefore) + } + } + ).last + + val memories = + scala.Predef.Map( + "scala.List" -> benchmark(scala.List.fill(_)(obj)), + "List" -> benchmark(List.fill(_)(obj)), + "LazyList" -> benchmark(LazyList.fill(_)(obj)), + "ArrayBuffer" -> benchmark(ArrayBuffer.fill(_)(obj)), + "ListBuffer" -> benchmark(ListBuffer.fill(_)(obj)) + ) + + // Print the results as a CSV document + println("Collection;" + sizes.mkString(";")) + for ((name, values) <- memories) { + println(name + ";" + values.map(_._2).mkString(";")) + } + +} diff --git a/benchmarks/time/src/main/scala/strawman/collection/immutable/LazyListBenchmark.scala b/benchmarks/time/src/main/scala/strawman/collection/immutable/LazyListBenchmark.scala new file mode 100644 index 0000000000..024e51d1b8 --- /dev/null +++ b/benchmarks/time/src/main/scala/strawman/collection/immutable/LazyListBenchmark.scala @@ -0,0 +1,58 @@ +package strawman.collection.immutable + +import java.util.concurrent.TimeUnit + +import org.openjdk.jmh.annotations._ +import scala.{Any, AnyRef, Int, Unit} + +@BenchmarkMode(scala.Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@Warmup(iterations = 12) +@Measurement(iterations = 12) +@State(Scope.Benchmark) +class LazyListBenchmark { + + @Param(scala.Array("8", "64", "512", "4096", "32768", "262144"/*, "2097152"*/)) + var size: Int = _ + + var xs: LazyList[AnyRef] = _ + var obj: Any = _ + + @Setup(Level.Trial) + def initData(): Unit = { + xs = LazyList.fill(size)("") + obj = "" + } + + @Benchmark + def cons(): Any = { + var ys = LazyList.empty[Any] + var i = 0 + while (i < size) { + ys = obj #:: ys + i += 1 + } + ys + } + + @Benchmark + def uncons(): Any = xs.tail + + @Benchmark + def concat(): Any = xs ++ xs + + @Benchmark + def foreach(): Any = { + var n = 0 + xs.foreach(x => if (x eq null) n += 1) + n + } + + @Benchmark + def lookup(): Any = xs(size - 1) + + @Benchmark + def map(): Any = xs.map(x => if (x eq null) "foo" else "bar") + +} diff --git a/benchmarks/time/src/main/scala/strawman/collection/immutable/ListBenchmark.scala b/benchmarks/time/src/main/scala/strawman/collection/immutable/ListBenchmark.scala new file mode 100644 index 0000000000..eb7d6c0fb7 --- /dev/null +++ b/benchmarks/time/src/main/scala/strawman/collection/immutable/ListBenchmark.scala @@ -0,0 +1,70 @@ +package strawman.collection.immutable + +import java.util.concurrent.TimeUnit + +import org.openjdk.jmh.annotations._ + +import scala.{Any, AnyRef, Int, Unit} + +@BenchmarkMode(scala.Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@Warmup(iterations = 12) +@Measurement(iterations = 12) +@State(Scope.Benchmark) +class ListBenchmark { + + @Param(scala.Array("8", "64", "512", "4096", "32768", "262144"/*, "2097152"*/)) + var size: Int = _ + + var xs: List[AnyRef] = _ + var obj: Any = _ + + @Setup(Level.Trial) + def initData(): Unit = { + xs = List.fill(size)("") + obj = "" + } + + @Benchmark + def cons(): Any = { + var ys = List.empty[Any] + var i = 0 + while (i < size) { + ys = obj :: ys + i += 1 + } + ys + } + + @Benchmark + def uncons(): Any = xs.tail + + @Benchmark + def concat(): Any = xs ++ xs + + @Benchmark + def foreach(): Any = { + var n = 0 + xs.foreach(x => if (x eq null) n += 1) + n + } + + @Benchmark + def foreach_while(): Any = { + var n = 0 + var ys = xs + while (ys.nonEmpty) { + if (ys.head eq null) n += 1 + ys = ys.tail + } + n + } + + @Benchmark + def lookup(): Any = xs(size - 1) + + @Benchmark + def map(): Any = xs.map(x => if (x eq null) "foo" else "bar") + +} diff --git a/benchmarks/time/src/main/scala/strawman/collection/immutable/ScalaListBenchmark.scala b/benchmarks/time/src/main/scala/strawman/collection/immutable/ScalaListBenchmark.scala new file mode 100644 index 0000000000..7d04279780 --- /dev/null +++ b/benchmarks/time/src/main/scala/strawman/collection/immutable/ScalaListBenchmark.scala @@ -0,0 +1,58 @@ +package strawman.collection.immutable + +import java.util.concurrent.TimeUnit + +import org.openjdk.jmh.annotations._ +import scala.{Any, AnyRef, Int, Unit} + +@BenchmarkMode(scala.Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@Warmup(iterations = 12) +@Measurement(iterations = 12) +@State(Scope.Benchmark) +class ScalaListBenchmark { + + @Param(scala.Array("8", "64", "512", "4096", "32768", "262144"/*, "2097152"*/)) + var size: Int = _ + + var xs: scala.List[AnyRef] = _ + var obj: Any = _ + + @Setup(Level.Trial) + def initData(): Unit = { + xs = scala.List.fill(size)("") + obj = "" + } + + @Benchmark + def cons(): Any = { + var ys = scala.List.empty[Any] + var i = 0 + while (i < size) { + ys = obj :: ys + i += 1 + } + ys + } + + @Benchmark + def uncons(): Any = xs.tail + + @Benchmark + def concat(): Any = xs ++ xs + + @Benchmark + def foreach(): Any = { + var n = 0 + xs.foreach(x => if (x eq null) n += 1) + n + } + + @Benchmark + def lookup(): Any = xs(size - 1) + + @Benchmark + def map(): Any = xs.map(x => if (x eq null) "foo" else "bar") + +} diff --git a/benchmarks/time/src/main/scala/strawman/collection/mutable/ArrayBufferBenchmark.scala b/benchmarks/time/src/main/scala/strawman/collection/mutable/ArrayBufferBenchmark.scala new file mode 100644 index 0000000000..d525d09496 --- /dev/null +++ b/benchmarks/time/src/main/scala/strawman/collection/mutable/ArrayBufferBenchmark.scala @@ -0,0 +1,59 @@ +package strawman.collection.mutable + +import java.util.concurrent.TimeUnit + +import org.openjdk.jmh.annotations._ +import strawman.collection.mutable.ArrayBuffer +import scala.{Any, AnyRef, Int, Unit} + +@BenchmarkMode(scala.Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@Warmup(iterations = 12) +@Measurement(iterations = 12) +@State(Scope.Benchmark) +class ArrayBufferBenchmark { + + @Param(scala.Array("8", "64", "512", "4096", "32768", "262144"/*, "2097152"*/)) + var size: Int = _ + + var xs: ArrayBuffer[AnyRef] = _ + var obj: Any = _ + + @Setup(Level.Trial) + def initData(): Unit = { + xs = ArrayBuffer.fill(size)("") + obj = "" + } + + @Benchmark + def cons(): Any = { + var ys = ArrayBuffer.empty[Any] + var i = 0 + while (i < size) { + ys += obj + i += 1 + } + ys + } + + @Benchmark + def uncons(): Any = xs.tail + + @Benchmark + def concat(): Any = xs ++ xs + + @Benchmark + def foreach(): Any = { + var n = 0 + xs.foreach(x => if (x eq null) n += 1) + n + } + + @Benchmark + def lookup(): Any = xs(size - 1) + + @Benchmark + def map(): Any = xs.map(x => if (x eq null) "foo" else "bar") + +} diff --git a/benchmarks/time/src/main/scala/strawman/collection/mutable/ListBufferBenchmark.scala b/benchmarks/time/src/main/scala/strawman/collection/mutable/ListBufferBenchmark.scala new file mode 100644 index 0000000000..6e85c1b0a3 --- /dev/null +++ b/benchmarks/time/src/main/scala/strawman/collection/mutable/ListBufferBenchmark.scala @@ -0,0 +1,58 @@ +package strawman.collection.mutable + +import java.util.concurrent.TimeUnit + +import org.openjdk.jmh.annotations._ +import scala.{Any, AnyRef, Int, Unit} + +@BenchmarkMode(scala.Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Fork(1) +@Warmup(iterations = 12) +@Measurement(iterations = 12) +@State(Scope.Benchmark) +class ListBufferBenchmark { + + @Param(scala.Array("8", "64", "512", "4096", "32768", "262144"/*, "2097152"*/)) + var size: Int = _ + + var xs: ListBuffer[AnyRef] = _ + var obj: Any = _ + + @Setup(Level.Trial) + def initData(): Unit = { + xs = ListBuffer.fill(size)("") + obj = "" + } + + @Benchmark + def cons(): Any = { + var ys = ListBuffer.empty[Any] + var i = 0 + while (i < size) { + ys += obj + i += 1 + } + ys + } + + @Benchmark + def uncons(): Any = xs.tail + + @Benchmark + def concat(): Any = xs ++ xs + + @Benchmark + def foreach(): Any = { + var n = 0 + xs.foreach(x => if (x eq null) n += 1) + n + } + + @Benchmark + def lookup(): Any = xs(size - 1) + + @Benchmark + def map(): Any = xs.map(x => if (x eq null) "foo" else "bar") + +} diff --git a/build.sbt b/build.sbt index 4f3a8b7356..bb89e4fde8 100644 --- a/build.sbt +++ b/build.sbt @@ -1,20 +1,32 @@ -organization := "org.scala-lang" +organization in ThisBuild := "org.scala-lang" -name := "collections" +version in ThisBuild := "0.1-SNAPSHOT" -version := "0.1-SNAPSHOT" +scalaVersion in ThisBuild := "2.12.1" -scalaVersion := "2.12.1" +scalacOptions in ThisBuild ++= + Seq("-deprecation", "-unchecked", "-Yno-imports", "-language:higherKinds") -scalacOptions ++= Seq("-deprecation", "-unchecked", "-Yno-imports", "-language:higherKinds") - -testOptions += Tests.Argument(TestFrameworks.JUnit, "-q", "-v", "-s", "-a") +testOptions in ThisBuild += Tests.Argument(TestFrameworks.JUnit, "-q", "-v", "-s", "-a") fork in Test := true parallelExecution in Test := false -libraryDependencies ++= Seq( - "org.scala-lang.modules" %% "scala-java8-compat" % "0.8.0", - "com.novocode" % "junit-interface" % "0.11" % "test" -) +val collections = + project.in(file(".")) + .settings( + libraryDependencies ++= Seq( + "org.scala-lang.modules" %% "scala-java8-compat" % "0.8.0", + "com.novocode" % "junit-interface" % "0.11" % Test + ) + ) + +val timeBenchmark = + project.in(file("benchmarks/time")) + .dependsOn(collections) + .enablePlugins(JmhPlugin) + +val memoryBenchmark = + project.in(file("benchmarks/memory")) + .dependsOn(collections) \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000000..4a50c6d517 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.20") \ No newline at end of file diff --git a/src/main/scala/strawman/collection/Iterable.scala b/src/main/scala/strawman/collection/Iterable.scala index 37195eae62..f6e241d5ba 100644 --- a/src/main/scala/strawman/collection/Iterable.scala +++ b/src/main/scala/strawman/collection/Iterable.scala @@ -45,6 +45,7 @@ trait FromIterable[+C[X] <: Iterable[X]] { trait IterableFactory[+C[X] <: Iterable[X]] extends FromIterable[C] { def empty[X]: C[X] = fromIterable(View.Empty) def apply[A](xs: A*): C[A] = fromIterable(View.Elems(xs: _*)) + def fill[A](n: Int)(elem: => A): C[A] = fromIterable(View.Fill(n)(elem)) } /** Operations over iterables. No operation defined here is generic in the @@ -71,6 +72,9 @@ trait IterableOps[+A] extends Any { /** Is the collection empty? */ def isEmpty: Boolean = !iterator().hasNext + /** Is the collection not empty? */ + def nonEmpty: Boolean = iterator().hasNext + /** The first element of the collection. */ def head: A = iterator().next() diff --git a/src/main/scala/strawman/collection/View.scala b/src/main/scala/strawman/collection/View.scala index 3eeb874f13..9ff13b9ae2 100644 --- a/src/main/scala/strawman/collection/View.scala +++ b/src/main/scala/strawman/collection/View.scala @@ -33,6 +33,20 @@ object View { override def knownSize = xs.length // should be: xs.knownSize, but A*'s are not sequences in this strawman. } + /** A view filled with `n` identical elements */ + case class Fill[A](n: Int)(elem: => A) extends View[A] { + def iterator() = + new Iterator[A] { + private var i = 0 + def hasNext: Boolean = i < n + def next(): A = { + i = i + 1 + if (i <= n) elem else Iterator.empty.next() + } + } + override def knownSize: Int = n + } + /** A view that filters an underlying collection. */ case class Filter[A](underlying: Iterable[A], p: A => Boolean) extends View[A] { def iterator() = underlying.iterator().filter(p) diff --git a/src/main/scala/strawman/collection/immutable/LazyList.scala b/src/main/scala/strawman/collection/immutable/LazyList.scala index 1e96a3dcbe..4780122c6a 100644 --- a/src/main/scala/strawman/collection/immutable/LazyList.scala +++ b/src/main/scala/strawman/collection/immutable/LazyList.scala @@ -18,6 +18,7 @@ class LazyList[+A](expr: => LazyList.Evaluated[A]) } override def isEmpty = force.isEmpty + override def nonEmpty = force.nonEmpty override def head = force.get._1 override def tail = force.get._2 diff --git a/src/main/scala/strawman/collection/immutable/List.scala b/src/main/scala/strawman/collection/immutable/List.scala index f332e64a1a..c5a70e8765 100644 --- a/src/main/scala/strawman/collection/immutable/List.scala +++ b/src/main/scala/strawman/collection/immutable/List.scala @@ -39,12 +39,14 @@ sealed trait List[+A] case class :: [+A](x: A, private[collection] var next: List[A @uncheckedVariance]) // sound because `next` is used only locally extends List[A] { override def isEmpty = false + override def nonEmpty = true override def head = x override def tail = next } case object Nil extends List[Nothing] { override def isEmpty = true + override def nonEmpty = false override def head = ??? override def tail = ??? }