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..06cdf5b02c --- /dev/null +++ b/benchmarks/time/src/main/scala/strawman/collection/mutable/ArrayBufferBenchmark.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 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..72f98d0e2e 100644 --- a/build.sbt +++ b/build.sbt @@ -1,20 +1,44 @@ -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) + .settings( + // runs the benchmarks and produce charts + InputKey[Unit]("charts") := { + val benchmarks = Def.spaceDelimited().parsed + val targetDir = crossTarget.value + val jmhReport = targetDir / "jmh-result.json" + val jmhArgs = s" -rf json -rff ${jmhReport.absolutePath} $benchmarks" + // HACK We should use `jmhArgs` here + val _ = (run in Jmh).partialInput(" -rf json -rff target/scala-2.12/jmh-result.json").evaluated + strawman.collection.Bencharts(jmhReport, targetDir) + } + ) + +val memoryBenchmark = + project.in(file("benchmarks/memory")) + .dependsOn(collections) \ No newline at end of file diff --git a/project/Bencharts.scala b/project/Bencharts.scala new file mode 100644 index 0000000000..16746909f9 --- /dev/null +++ b/project/Bencharts.scala @@ -0,0 +1,87 @@ +package strawman.collection + +import javax.imageio.ImageIO + +import org.jfree.chart.JFreeChart +import org.jfree.chart.axis.{LogAxis, NumberAxis} +import org.jfree.chart.plot.XYPlot +import org.jfree.chart.renderer.xy.XYErrorRenderer +import org.jfree.data.xy.{YIntervalSeries, YIntervalSeriesCollection} +import play.api.libs.json.{JsObject, Json} +import sbt._ + +object Bencharts { + + /** + * Generate charts from the result of a JMH execution. + * + * Benchmarks that have the same name (e.g. `cons`) are grouped + * into a single chart with one series for each. + * + * @param jmhReport JMH results report + * @param targetDir Directory in which the images will be written + */ + def apply(jmhReport: File, targetDir: File): Unit = { + val json = Json.parse(IO.read(jmhReport)) + + json.as[List[JsObject]] + .groupBy { result => + val name = (result \ "benchmark").as[String] + val benchmark = name.reverse.takeWhile(_ != '.').reverse + benchmark // Benchmark name (e.g. "cons", "foreach", "map") + } + .foreach { case (benchmark, results) => + val seriess = + results + // group by concrete collection type + .groupBy(result => (result \ "benchmark").as[String].stripSuffix(benchmark)) + .map { case (collectionType, iterations) => + val ySeries = new YIntervalSeries(collectionType) + // each benchmark has been run with several collection sizes (8, 64, 512, etc.) + // we add a point for each of these iterations + for (iteration <- iterations) { + ySeries.add( + (iteration \ "params" \ "size").as[String].toDouble, + (iteration \ "primaryMetric" \ "score").as[Double], + (iteration \ "primaryMetric" \ "scoreConfidence").apply(0).as[Double], + (iteration \ "primaryMetric" \ "scoreConfidence").apply(1).as[Double] + ) + } + ySeries + } + + val xAxis = new LogAxis("Size") + xAxis.setBase(2) + xAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()) + val yAxis = new LogAxis("Execution time (lower is better)") + + val col = new YIntervalSeriesCollection() + val renderer = new XYErrorRenderer + for ((series, i) <- seriess.zipWithIndex) { + col.addSeries(series) + renderer.setSeriesLinesVisible(i, true) + } + + val plot = new XYPlot( + col, + xAxis, yAxis, + renderer + ) + + val chart = new JFreeChart( + benchmark, + JFreeChart.DEFAULT_TITLE_FONT, + plot, + true + ) + + ImageIO.write( + chart.createBufferedImage(800, 600), + "png", + targetDir / s"$benchmark.png" + ) + + } + } + +} \ No newline at end of file diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000000..ac5f935d40 --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,7 @@ +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.2.20") + +// for bencharts +libraryDependencies ++= Seq( + "org.jfree" % "jfreechart" % "1.0.14", + "com.typesafe.play" %% "play-json" % "2.4.10" +) 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 = ??? } diff --git a/src/main/scala/strawman/collection/mutable/ArrayBuffer.scala b/src/main/scala/strawman/collection/mutable/ArrayBuffer.scala index 37c8d25853..c70df6ad97 100644 --- a/src/main/scala/strawman/collection/mutable/ArrayBuffer.scala +++ b/src/main/scala/strawman/collection/mutable/ArrayBuffer.scala @@ -1,7 +1,7 @@ package strawman.collection.mutable import java.lang.IndexOutOfBoundsException -import scala.{Array, Int, Long, Boolean, Unit, AnyRef} +import scala.{Array, Exception, Int, Long, Boolean, math, StringContext, Unit, AnyRef} import strawman.collection import strawman.collection.{IterableFactory, IterableOnce, SeqLike, IndexedView} import scala.Predef.intWrapper @@ -147,11 +147,14 @@ object RefArrayUtils { // Use a Long to prevent overflows val arrayLength: Long = array.length def growArray = { - var newSize: Long = arrayLength * 2 + var newSize: Long = math.max(arrayLength * 2, 8) while (n > newSize) newSize = newSize * 2 // Clamp newSize to Int.MaxValue - if (newSize > Int.MaxValue) newSize = Int.MaxValue + if (newSize > Int.MaxValue) { + if (end == Int.MaxValue) throw new Exception(s"Collections can not have more than ${Int.MaxValue} elements") + newSize = Int.MaxValue + } val newArray: Array[AnyRef] = new Array(newSize.toInt) Array.copy(array, 0, newArray, 0, end)