我尝试使用Scala的并行集合来并行调度一些计算。因为有很多输入数据,所以我使用可变数组来存储数据以避免GC问题。这是我采取的最初方法:
// initialize the reusable input data structure
val inputData = new Array[Array[Int]](Runtime.getRuntime.availableProcessors*ChunkSize)
for (i <- 0 until inputData.length) {
inputData(i) = new Array[Int](arraySize)
}
// process the input
while (haveMoreInput()) {
// read the input--must be sequential!
for (array <- 0 until inputData.length) {
for (index <- 0 until arraySize) {
array(index) = deserializeFromExternalSource()
}
}
// map the data in parallel
// note that the input data is NOT modified by longRuningProcess
val results = for (array <- inputData.par) yield {
longRunningProcess(array)
}
// use the results--must be sequential and ordered as input
for (result <- results.toArray) {
useResult(result)
}
}
鉴于可以安全地重用ParallelArray
的基础数组(即,修改并用作另一个ParallelArray
的基础结构),上面的剪辑应该按预期工作。但是,运行时它会因内存错误而崩溃:
*** Error in `*** Error in `java': double free or corruption (fasttop): <memory address> ***
这表面上与并行集合直接使用它创建的数组有关;也许它在超出范围时试图释放这个数组。在任何情况下,由于内存限制,每个循环创建一个新数组也不是一个选项。在var parInputData = inputData.par
循环的内部和外部明确创建while
会导致相同的双重自由错误。
我不能简单地将inputData
本身作为并行集合,因为它需要按顺序填充(尝试对并行版本进行分配,我意识到分配没有按顺序执行)。使用Vector
作为外部数据结构似乎适用于相对较小的输入大小(&lt; 1000000输入数组),但会导致大型输入上的GC开销异常。
我最终参与制作Vector[Vector[Array[Int]]]
的方法,外部向量的长度等于所使用的并行线程的数量。然后我用一大块输入数据数组手动填充每个子Vector
,然后在外部矢量上做一个平行映射。
这种最终方法有效,但手动将输入分成块并将这些块添加到另一层深度的并行集合是很繁琐的。有没有办法允许Scala重用可变数组进行并行操作?
编辑:使用同步队列对上面的并行向量解决方案与手动并行化解决方案进行基准测试表明,并行向量的速度要慢50%。我想知道这是否只是更好抽象的开销,或者是否可以通过使用并行数组而不是Vector
来减少这种差距;这将导致使用数组与Vector
的另一个好处。
答案 0 :(得分:3)
将数据拆分成块是没有意义的,Parallel Collections库的重点在于它可以为您完成,并且比使用固定块大小做得更好。此外,JVM上的数组数组与C中的数组数组不同,它们更像是指向许多小数组的指针数组,这使得它们效率低下。
更优雅的解决方法是使用普通Array
并使用ParRange
对其进行操作。必须将longRunningProcess
更改为一次对单个元素进行操作:
val arraySize = ???
val inputData = Array[Int](arraySize)
val outputData = Array[ResultType](arraySize)
while(haveMoreInput()) {
for (i <- 0 until arraySize)
inputData(i) = deserializeFromExternalSource()
for (i <- (0 until arraySize).par)
outputData(i) = longRunningProcess(inputData(i))
outputData.foreach(useResult)
}
这只使用两个大型数组,从不分配任何新数组。 ParArray.map
,ParArray.toArray
和Array.par
在原始代码中分配了新数组。
我们仍然必须使用固定的arraySize
来确保我们不会将更多数据加载到我们有空间的内存中。更好的解决方案是使用reactive streams,但它们还没有准备好进行生产。