【问题标题】:Efficiency/scalability of parallel collections in Scala (graphs)Scala 中并行集合的效率/可扩展性(图表)
【发布时间】:2012-03-16 18:53:08
【问题描述】:

所以我一直在 Scala 中为我正在处理的图形项目使用并行集合,我已经定义了图形类的基础知识,它目前使用的是 scala.collection.mutable.HashMap,其中键是 @987654324 @ 并且值为ListBuffer[Int](邻接列表)。 (编辑:此后已更改为ArrayBuffer[Int]

几个月前我在 C++ 中做过类似的事情,std::vector<int, std::vector<int> >

我现在要做的是在图中的所有顶点对之间运行一个度量,所以在 C++ 中我做了这样的事情:

// myVec = std::vector<int> of vertices
for (std::vector<int>::iterator iter = myVec.begin(); iter != myVec.end(); ++iter) {
    for (std::vector<int>::iterator iter2 = myVec.begin(); 
        iter2 != myVec.end(); ++iter2) {
        /* Run algorithm between *iter and *iter2 */
    }
}

我在 Scala 中做了同样的事情,并行化,(或试图)这样做:

// vertexList is a List[Int] (NOW CHANGED TO Array[Int] - see below)
vertexList.par.foreach(u =>
  vertexList.foreach(v =>
    /* Run algorithm between u and v */
  )
)

C++ 版本显然是单线程的,Scala 版本有.par,所以它使用并行集合并且在 8 核(同一台机器)上是多线程的。然而,C++ 版本在大约 3 天内处理了 305,570 对,而 Scala 版本迄今为止仅在 17 小时内处理了 23,573 对。

假设我的math 正确,单线程 C++ 版本比 Scala 版本快大约 3 倍。 Scala 真的比 C++ 慢得多,还是我完全误用了 Scala(我最近才开始使用 Scala 编程大约有 300 页)?

谢谢! -kstruct

编辑要使用 while 循环,我会做类似的事情吗..

// Where vertexList is an Array[Int]
vertexList.par.foreach(u =>
  while (i <- 0 until vertexList.length) {
    /* Run algorithm between u and vertexList(i) */
  }
}

如果你们的意思是对整个事情使用 while 循环,是否有相当于 .par.foreach 的 while?

EDIT2 等一下,那个代码甚至都不对——我的错。我将如何使用 while 循环并行化它?如果我有一些 var i 来跟踪迭代,那么不是所有线程都共享 i 吗?

【问题讨论】:

  • 这对我来说似乎太慢了。但是,如果没有更多信息,很难知道。内循环平均需要多长时间才能完成?我会尝试使用 YourKit 分析 scala 应用程序(单线程),以查看您的算法中的某些内容是否花费了令人惊讶的时间。
  • 尝试分析它。我猜是因为拳击。 ListBuffer[Int] 只能存储装箱的整数。尝试切换到Array[Int]
  • 它需要是一个 ArrayBuffer[Int] 对,因为用户可能决定添加/删除边缘?
  • 好读.. 但是,我将如何并行化 while 循环?

标签: scala graph scala-collections parallel-collections


【解决方案1】:

从您的 cmets 中,我看到您在每个算法运行结束时更新了共享可变 HashMap。如果你随机化你的步行,共享的Random 也是一个争论点。

我推荐两个改变:

  1. 使用 .map.flatMap 返回不可变集合,而不是修改共享集合。
  2. 使用ThreadLocalRandom(来自AkkaJava 7)来减少随机数生成器的争用
  3. 检查算法的其余部分以了解更多可能的争用点。
  4. 您也可以尝试并行运行内部循环。但是,如果不了解您的算法,就很难知道这是否会有所帮助或有害。幸运的是,运行并行和顺序集合的所有组合非常简单;只需在下面的代码中切换出pVertexListvertexList

类似这样的:

val pVertexList = vertexList.par
val allResult = for {
  u <- pVertexList
  v <- pVertexList
} yield {
  /* Run algorithm between u and v */
  ((u -> v) -> result)
}

allResult 将是ParVector[((Int, Int), Int)]。您可以在其上调用.toMap 将其转换为Map

【讨论】:

  • 我认为让它“更加并行”会适得其反。 +1 争用想法。
  • 嗯..我怎么能确定是否存在争用(不熟悉这个概念,只是做了一个快速的维基百科搜索)?我的算法本质上是一对顶点之间的随机游走,仅此而已......另外,做你发布的内容和做“普通”vertexList.par.foreach(u =&gt; vertexList.foreach(v =&gt; ... 之间有什么区别?
  • @kstruct 它必须对这些随机游走做一些事情,例如将数据存储在某处或将其打印出来。当您拥有在线程之间共享的可变状态时,您必须拥有some mechanism 以确保写入不会相互干扰,并且读取不会读取半写入的数据。多线程编程 + 共享可变状态极其难以正确执行和推理,并且会完全破坏并行集合的任何好处。
  • re:我的代码和你的代码的区别,.par 并没有修改原来的集合,而是返回一个新的并行集合。在我的版本中,内循环和外循环都是平行的;在你的只有外循环。不过,正如@DanielC.Sobral 所说,如果算法中存在争用,那么世界上所有的并行性都无济于事。
  • @kstruct 至于发现争用,寻找共享可变状态的使用,比如I/O、共享对象、互斥体、同步方法等。
【解决方案2】:

为什么是可变的?我认为 Scala 2.9.x 上没有一个好的并行可变映射——特别是因为即将推出的 Scala 2.10 中添加了这样的数据结构。

另一方面...你有一个List[Int]?不要使用它,使用Vector[Int]。另外,您确定没有在其他地方浪费时间,将可变映射和缓冲区转换为不可变列表吗? Scala 数据结构与 C++ 不同,因此您很可能会在代码的其他地方遇到复杂性问题。

最后,我认为dave 可能会在他问及争用时提出一些问题。如果你有争论,并行性很可能会让事情变慢。如果您使其并行运行,它的运行速度有多快/慢?如果使其不并行使其更快,那么您很可能确实存在争用问题。

【讨论】:

  • 为什么在这个例子中推荐VectorList 看起来不错,如果他只是 foreach-ing。
  • 我让它是可变的,因为用户可能想要随时添加顶点——我当然是 Scala 的新手,所以也许有更好的方法来处理这个问题。我应该注意,我倾向于处理大图(至少几千个顶点),所以效率/可扩展性是关键。至于在其他地方浪费时间,我相当确定 - 计算成本最高的部分本质上是在成对的顶点之间进行随机游走,这不会改变任何数据。
  • @schmmd .par 调用将创建一个新的并行集合。对于ListVector,此时创建的集合都是ParVector(静态类型不同,但实现类相同)。 ParVector 只使用底层的Vector 和覆盖方法,因此将Vector 转换为O(1),而将List 转换为O(n)。事实上,任何将 List 转换为的并行集合都是 O(n),因为 List 不适合并行性。
  • 目前我使用的是Array[Int],而不是Vector[Int].. 两者之间有什么区别?我认为Vectors 类似于Lists(即不可变)但允许 O(1) 随机访问?
  • @kstruct 在你的问题中你说vertexList is a List[Int]。当我们没有得到正确的信息时,很难就某些代码提出建议。 Vector 是不可变的,它接近于 O(1) 随机访问,但 Array 由于 JVM 处理它的方式而具有其自身的特性。但是一旦你调用.par,你就会得到一个新的集合,这个集合的生成方式很重要。
【解决方案3】:

我对此并不完全确定,但我认为 foreach 循环中的 foreach 循环相当慢,因为会创建很多对象。见:http://scala-programming-language.1934581.n4.nabble.com/for-loop-vs-while-loop-performance-td1935856.html

尝试使用 while 循环重写它。

此外,列表仅对头部访问有效,数组可能更快。

【讨论】:

  • 我应该将保存图形顶点的 List[Int] 更改为 Array[Int],还是应该将邻接列表的 ListBuffer[Int] 更改为 ArrayBuffer[Int],或者两者都有?
  • 是的,尝试使用数组并在迭代器上使用 while 循环而不是 foreach 循环。但是看看你在循环中实际做了什么也会很有趣。也许还有更多的优化空间。编辑:在同一分钟回答;-)...两者都使用它,或者玩一下
  • 要使用一个while循环,你的意思是像(见上文)。
  • 链接的线程已经很老了。列表对迭代很有效(您所做的只是在尾列表上一遍又一遍地进行头部访问),但数组可能对内存局部性有好处。
  • 废话。您将失去并行性。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-11-23
  • 1970-01-01
  • 2017-04-03
  • 2016-07-08
  • 2014-11-15
  • 2015-12-11
相关资源
最近更新 更多