Scala快速并行化集合的方式

时间:2014-03-31 09:22:45

标签: scala parallel-processing

我的代码相当于:

def iterate(prev: Vector[Int], acc: Int): Vector[Int] = {
  val next = (for { i <- 1.to(1000000) }
    yield (prev(Random.nextInt(i))) ).toVector

  if (acc < 20) iterate(next, acc + 1)
  else next
}
iterate(1.to(1000000).toVector, 1)

对于大量迭代,它对集合执行操作,并生成值。在迭代结束时,它将所有内容转换为向量。最后,它继续进行下一个递归自调用,但在完成所有迭代之前无法继续。递归自我调用的次数非常少。

我想对此进行解释,所以我尝试在1.to(1000000)范围内使用.par。这使用了8个进程而不是1个,结果只快了两倍! .toParArray仅比.par略快。有人告诉我,如果我使用不同的东西,比如ThreadPool,可能要快得多 - 这是有道理的,因为所有的时间都花在构建next上,我假设将不同进程的输出连接到共享内存即使对于非常大的输出,也不会导致巨大的减速(这是一个关键假设,可能是错误的)。我该怎么做?如果你提供代码,那么对我提供的代码进行并列化就足够了。

请注意,我提供的代码不是我的实际代码。我的实际代码更长且更复杂(带有约束,BitSets和更多内容的TSP的Held-Karp算法),唯一值得注意的区别是,在我的代码中,prev的类型是ParMap而不是Vector

编辑,额外信息:ParMap在我能处理的最大样本量的最差迭代上有350k个元素,否则它通常为5k-200k(在对数刻度上变化)。如果它本身需要花费大量时间将流程中的结果连接到一个单独的流程中(我假设这就是正在发生的事情),那么我无能为力,但我很怀疑是这种情况。

1 个答案:

答案 0 :(得分:0)

在问题中提出的原文之后实现了几个版本,

因此

import scala.collection.mutable.ArrayBuffer
import scala.collection.parallel.mutable.ParArray
import scala.util.Random

// Original
def rec0() = {
  def iterate(prev: Vector[Int], acc: Int): Vector[Int] = {
    val next = (for { i <- 1.to(1000000) }
      yield (prev(Random.nextInt(i))) ).toVector

    if (acc < 20) iterate(next, acc + 1)
    else next
  }
  iterate(1.to(1000000).toVector, 1)
}

//  par map
def rec1() = {
  def iterate(prev: Vector[Int], acc: Int): Vector[Int] = {
    val next = (1 to 1000000).par.map { i => prev(Random.nextInt(i)) }.toVector

    if (acc < 20) iterate(next, acc + 1)
    else next
  }
  iterate(1.to(1000000).toVector, 1)
}

// ParArray par map
def rec2() = {
  def iterate(prev: ParArray[Int], acc: Int): ParArray[Int] = {
    val next = (1 to 1000000).par.map { i => prev(Random.nextInt(i)) }.toParArray

    if (acc < 20) iterate(next, acc + 1)
    else next
  }
  iterate((1 to 1000000).toParArray, 1).toVector
}

// Non-idiomatic non-parallel
def rec3() = {
  def iterate(prev: ArrayBuffer[Int], acc: Int): ArrayBuffer[Int] = {

    var next = ArrayBuffer.tabulate(1000000){i => i+1}
    var i = 0
    while (i < 1000000) {
      next(i) = prev(Random.nextInt(i+1))
      i = i + 1
    }

    if (acc < 20) iterate(next, acc + 1)
    else next
  }
  iterate(ArrayBuffer.tabulate(1000000){i => i+1}, 1).toVector
}

然后对平均经过时间进行一点测试,

def elapsed[A] (f: => A): Double = {
  val start = System.nanoTime()
  f
  val stop = System.nanoTime()
  (stop-start)*1e-6d
}

val times = 10
val e0 = (1 to times).map { i => elapsed(rec0) }.sum / times
val e1 = (1 to times).map { i => elapsed(rec1) }.sum / times
val e2 = (1 to times).map { i => elapsed(rec2) }.sum / times
val e3 = (1 to times).map { i => elapsed(rec3) }.sum / times

// time in ms.
e0: Double = 2782.341
e1: Double = 2454.828
e2: Double = 3455.976
e3: Double = 1275.876

表明非惯用的非并行版本证明平均速度最快。也许对于较大的输入数据,并行的惯用版本可能是有益的。