Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Drastically speed up Monoid[Map[K, V]] instance. #1300

Merged
merged 4 commits into from
Aug 19, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions bench/src/main/scala/cats/bench/MapMonoidBench.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cats.bench

import cats.instances.list._
import cats.instances.int._
import cats.instances.map._

import scalaz.std.anyVal._
import scalaz.std.list._
import scalaz.std.map._

import org.openjdk.jmh.annotations.{ Benchmark, Scope, State }

@State(Scope.Benchmark)
class MapMonoidBench {

val words: List[String] =
for {
c <- List("a", "b", "c", "d", "e")
t <- 1 to 100
} yield c * t

val maps: List[Map[String, Int]] = (words ++ words).map(s => Map(s -> 1))

@Benchmark def combineAllCats: Map[String, Int] =
cats.Monoid[Map[String, Int]].combineAll(maps)

@Benchmark def combineCats: Map[String, Int] =
maps.foldLeft(Map.empty[String, Int]) {
case (acc, m) => cats.Monoid[Map[String, Int]].combine(acc, m)
}

@Benchmark def combineScalaz: Map[String, Int] =
maps.foldLeft(Map.empty[String, Int]) {
case (acc, m) => scalaz.Monoid[Map[String, Int]].append(acc, m)
}

@Benchmark def combineDirect: Map[String, Int] =
maps.foldLeft(Map.empty[String, Int]) {
case (acc, m) => m.foldLeft(acc) {
case (m, (k, v)) => m.updated(k, v + m.getOrElse(k, 0))
}
}

@Benchmark def combineGeneric: Map[String, Int] =
combineMapsGeneric[String, Int](maps, 0, _ + _)

def combineMapsGeneric[K, V](maps: List[Map[K, V]], z: V, f: (V, V) => V): Map[K, V] =
maps.foldLeft(Map.empty[K, V]) {
case (acc, m) => m.foldLeft(acc) {
case (m, (k, v)) => m.updated(k, f(v, m.getOrElse(k, z)))
}
}

@Benchmark def foldMapCats: Map[String, Int] =
cats.Foldable[List].foldMap(maps)(identity)

@Benchmark def foldMapScalaz: Map[String, Int] =
scalaz.Foldable[List].foldMap(maps)(identity)
}
2 changes: 2 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,8 @@ lazy val bench = project.dependsOn(macrosJVM, coreJVM, freeJVM, lawsJVM)
.settings(catsSettings)
.settings(noPublishSettings)
.settings(commonJvmSettings)
.settings(libraryDependencies ++= Seq(
"org.scalaz" %% "scalaz-core" % "7.2.5"))
.enablePlugins(JmhPlugin)

// cats-js is JS-only
Expand Down
3 changes: 3 additions & 0 deletions kernel/src/main/scala/cats/kernel/Monoid.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ trait Monoid[@sp(Int, Long, Float, Double) A] extends Any with Semigroup[A] {
*/
def combineAll(as: TraversableOnce[A]): A =
as.foldLeft(empty)(combine)

override def combineAllOption(as: TraversableOnce[A]): Option[A] =
if (as.isEmpty) None else Some(combineAll(as))
}

abstract class MonoidFunctions[M[T] <: Monoid[T]] extends SemigroupFunctions[M] {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
package cats.kernel
package instances.util
package instances

import scala.collection.mutable

object StaticMethods {

def initMutableMap[K, V](m: Map[K, V]): mutable.Map[K, V] = {
val result = mutable.Map.empty[K, V]
m.foreach { case (k, v) => result(k) = v }
result
}

def wrapMutableMap[K, V](m: mutable.Map[K, V]): Map[K, V] =
new WrappedMutableMap(m)

Expand All @@ -22,19 +16,6 @@ object StaticMethods {
def -(key: K): Map[K, V] = m.toMap - key
}

// the caller should arrange so that the smaller map is the first
// argument, and the larger map is the second.
def addMap[K, V](small: Map[K, V], big: Map[K, V])(f: (V, V) => V): Map[K, V] = {
val m = initMutableMap(big)
small.foreach { case (k, v1) =>
m(k) = m.get(k) match {
case Some(v2) => f(v1, v2)
case None => v1
}
}
wrapMutableMap(m)
}

// scalastyle:off return
def iteratorCompare[A](xs: Iterator[A], ys: Iterator[A])(implicit ev: Order[A]): Int = {
while (true) {
Expand Down
23 changes: 20 additions & 3 deletions kernel/src/main/scala/cats/kernel/instances/map.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package cats.kernel
package instances

import cats.kernel.instances.util.StaticMethods.addMap
import scala.collection.mutable

package object map extends MapInstances

Expand All @@ -24,12 +24,29 @@ class MapEq[K, V](implicit V: Eq[V]) extends Eq[Map[K, V]] {
}

class MapMonoid[K, V](implicit V: Semigroup[V]) extends Monoid[Map[K, V]] {

def empty: Map[K, V] = Map.empty

def combine(xs: Map[K, V], ys: Map[K, V]): Map[K, V] =
if (xs.size <= ys.size) {
addMap(xs, ys)((x, y) => V.combine(x, y))
xs.foldLeft(ys) { case (my, (k, x)) =>
my.updated(k, Semigroup.maybeCombine(x, my.get(k)))
}
} else {
addMap(ys, xs)((y, x) => V.combine(x, y))
ys.foldLeft(xs) { case (mx, (k, y)) =>
mx.updated(k, Semigroup.maybeCombine(mx.get(k), y))
}
}

override def combineAll(xss: TraversableOnce[Map[K, V]]): Map[K, V] = {
val acc = mutable.Map.empty[K, V]
xss.foreach { m =>
val it = m.iterator
while (it.hasNext) {
val (k, v) = it.next
m.updated(k, Semigroup.maybeCombine(m.get(k), v))
}
}
StaticMethods.wrapMutableMap(acc)
}
}
2 changes: 0 additions & 2 deletions kernel/src/main/scala/cats/kernel/instances/stream.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cats.kernel
package instances

import cats.kernel.instances.util.StaticMethods

package object stream extends StreamInstances

trait StreamInstances extends StreamInstances1 {
Expand Down
2 changes: 0 additions & 2 deletions kernel/src/main/scala/cats/kernel/instances/vector.scala
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package cats.kernel
package instances

import cats.kernel.instances.util.StaticMethods

package object vector extends VectorInstances

trait VectorInstances extends VectorInstances1 {
Expand Down