我有一张地图清单:
val list = List(
Map("id" -> "A", "value" -> 20, "name" -> "a"),
Map("id" -> "B", "value" -> 10, "name" -> "b"),
Map("id" -> "A", "value" -> 5, "name" -> "a"),
Map("id" -> "C", "value" -> 1, "name" -> "c"),
Map("id" -> "D", "value" -> 60, "name" -> "d"),
Map("id" -> "C", "value" -> 3, "name" -> "c")
)
我想对value
求和,并以最有效的方式将它们按id
值分组,以便它变为:
Map(A -> 25, B -> 10, C -> 4, D -> 60)
答案 0 :(得分:7)
A)如果你有很多具有相同身份的项目,那么这个项目的可读性和性能最高:
scala> list.groupBy(_("id")).mapValues(_.map(_("value").asInstanceOf[Int]).sum)
res14: scala.collection.immutable.Map[Any,Int] = Map(D -> 60, A -> 25, C -> 4, B -> 10)
您也可以使用list.groupBy(_("id")).par...
。只有当你有许多具有相同键的元素时,它才会更快地工作,否则它会过于缓慢。
否则,更改线程的上下文本身会使.par
版本变慢,因为map(_"value").sum
(您的嵌套map-reduce)可能比在线程之间切换更快。如果N
=系统中的核心数量,那么map {reduce应该慢N
倍,以便从par
中受益,当然,大致可以说。
B)因此,如果并行化不能很好地工作(最好通过性能测试来检查),你可以用专门的方式“重新实现”groupBy
:
val m = scala.collection.mutable.Map[String, Int]() withDefaultValue(0)
for (e <- list; k = e("id").toString) m.update(k, m(k) + e("value").asInstanceOf[Int])
C)最平行的选项是:
val m = new scala.collection.concurrent.TrieMap[String, Int]()
for (e <- list.par; k = e("id").toString) {
def replace = {
val v = m(k)
m.replace(k, v, v + e("value").asInstanceOf[Int]) //atomic
}
m.putIfAbsent(k, 0) //atomic
while(!replace){} //in case of conflict
}
scala> m
res42: scala.collection.concurrent.TrieMap[String,Int] = TrieMap(B -> 10, C -> 4, D -> 60, A -> 25)
D)功能样式中并行化程度最高(每次合并映射速度较慢,但对于没有共享内存的分布式map-reduce最佳),使用scalaz semigroups:
import scalaz._; import Scalaz._
scala> list.map(x => Map(x("id").asInstanceOf[String] -> x("value").asInstanceOf[Int]))
.par.reduce(_ |+| _)
res3: scala.collection.immutable.Map[String,Int] = Map(C -> 4, D -> 60, A -> 25, B -> 10)
但只有当你使用比“+”更复杂的聚合时,它才会更有效。
让我们做简单的性能测试:
def time[T](n: Int)(f: => T) = {
val start = System.currentTimeMillis()
for(i <- 1 to n) f
(System.currentTimeMillis() - start).toDouble / n
}
在MacBook Pro 2.3 GHz Intel Core i7上使用JDK8在Scala 2.12 REPL中完成。每次测试都会启动两次 - 首先是为JVM预热。
1)对于您的输入集合和time(100000){...}
,从最慢到最快:
`par.groupBy.par.mapValues` = 0.13861 ms
`groupBy.par.mapValues` = 0.07667 ms
`most parallelized` = 0.06184 ms
`scalaz par.reduce(_ |+| _)` = 0.04010 ms //same for other reduce-based implementations, mentioned here
`groupBy.mapValues` = 0.00212 ms
`for` + `update` with mutable map initialization time = 0.00201 ms
`scalaz suml` = 0.00171 ms
`foldLeft` from another answer = 0.00114 ms
`for` + `update` without mutable map initialization time = 0.00105
所以,来自另一个答案的foldLeft
似乎是您输入的最佳解决方案。
2)让它变得更大
scala> val newlist = (1 to 1000).map(_ => list).reduce(_ ++ _)
现在输入newList
并time(1000){...}
:
`scalaz par.reduce(_ |+| _)` = 1.422 ms
`foldLeft`/`for` = 0.418 ms
`groupBy.par.mapValues` = 0.343 ms
最好在这里选择groupBy.par.mapValues
。
3)最后,让我们定义另一个聚合:
scala> implicit class RichInt(i: Int){ def ++ (i2: Int) = { Thread.sleep(1); i + i2}}
defined class RichInt
使用list
和time(1000)
进行测试:
`foldLeft` = 7.742 ms
`most parallelized` = 3.315 ms
所以最好在这里使用大多数并行版本。
为什么减速是如此缓慢:
让我们采取8个要素。它从叶子[1] + ... + [1]
到根[1 + ... + 1]
生成一个计算树:
time(([1] + [1]) + ([1] + [1]) + ([1] + [1]) + ([1] + [1])
=> ([1 +1] + [1 +1]) + ([1 + 1] + [1 + 1])
=> [1 + 1 + 1 + 1] + [1 + 1 + 1 + 1])
= (1 + 1 + 1 + 1) + (2 + 2) + 4 = 12
时间(N = 8)= 8/2 + 2 * 8/4 + 4 * 8/8 = 8 *(1/2 + 2/4 + 4/8)= 8 * log2(8)/ 2 = 12
或者只是:
当然,此公式仅适用于实际上为2的幂。无论如何,复杂度为O(NlogN)
,慢于foldLeft
的{{1}}。即使在并行化之后,它也只是O(N)
所以这个实现只能用于Big Data的分布式Map-Reduce,或者简单地说当你没有足够的内存并将Map存储在某个缓存中时。
您可能会注意到它的并行化程度比输入的其他选项更好 - 这只是因为对于6个元素来说它不是那么慢(这里几乎是O(N)
) - 并且当只有其他选项进行分组时,您只进行一次减少调用数据之前或只是创建更多线程,这会导致更多的“线程切换”开销。简单地说,O(1)
在这里创建的线程更少。但是如果你有更多的数据 - 它当然不起作用(参见实验2)。
答案 1 :(得分:5)
还使用foldLeft
:
list.foldLeft(Map[String, Int]().withDefaultValue(0))((res, v) => {
val key = v("id").toString
res + (key -> (res(key) + v("value").asInstanceOf[Int]))
})
更新:与reduceLeft
:
(Map[String, Any]().withDefaultValue(0) :: list).reduceLeft((res, v) => {
val key = v("id").toString
res + (key -> (res(key).asInstanceOf[Int] + v("value").asInstanceOf[Int]))
})
顺便说一下,如果你看一下reduceLeft
定义,你就会发现它使用相同的foldLeft
:
def reduceLeft[B >: A](f: (B, A) => B): B =
if (isEmpty) throw new UnsupportedOperationException("empty.reduceLeft")
else tail.foldLeft[B](head)(f)
更新2:与par
和reduce
:
这里的问题是将结果Map值与初始Map值区分开来。我选择了contains("id")
。
list.par.reduce((a, b) => {
def toResultMap(m: Map[String, Any]) =
if (m.contains("id"))
Map(m("id").toString -> m("value")).withDefaultValue(0)
else m
val aM = toResultMap(a)
val bM = toResultMap(b)
aM.foldLeft(bM)((res, v) =>
res + (v._1 -> (res(v._1).asInstanceOf[Int] + v._2.asInstanceOf[Int])))
})
答案 2 :(得分:3)
我不知道&#34;效率最高&#34;,但我能想到的最好的方法是使用scalaz suml
,它使用Monoid
; Monoid
的{{1}}完全符合您的要求。唯一令人遗憾的部分是将Map
转换为更好的类型并表示我们想要的结构(例如Map[String, Any]
)。
Map("A" → 20)
答案 3 :(得分:0)
从Scala 2.13
开始,您可以使用groupMapReduce
方法,该方法(如其名称所示)等效于groupBy
后跟mapValues
和{{1} }步骤:
reduce
此:
// val list = List(Map("id" -> "A", "value" -> 20, "name" -> "a"), Map("id" -> "B", "value" -> 10, "name" -> "b"), Map("id" -> "A", "value" -> 5, "name" -> "a"), Map("id" -> "C", "value" -> 1, "name" -> "c"), Map("id" -> "D", "value" -> 60, "name" -> "d"), Map("id" -> "C", "value" -> 3, "name" -> "c"))
list.groupMapReduce(_("id"))(_("value").asInstanceOf[Int])(_ + _)
// Map("A" -> 25, "B" -> 10, "C" -> 4, "D" -> 60)
的“ id”字段(group
)的地图(组 MapReduce的组部分)
_("id")
将每个分组的Map映射到其“值”字段,然后键入Int(map
)( Map Reduce组的映射部分)
_("value").asInstanceOf[Int]
)中的值 reduce
进行求和(减少groupMap Reduce 的一部分)。
这是one-pass version可以翻译的内容:
_ + _