为什么Haskell列表推导不是并行执行的?

时间:2014-09-08 20:02:52

标签: haskell parallel-processing list-comprehension

我正在做项目欧拉问题21的家庭作业,我有这个列表理解:

amicableNumberSums = [ x+y | x<-[1..10000], y <-[1..10000], (amicable x y)]

这需要很长时间才能执行(可以理解,因为它测试10000 ^ 2对数字)并查看我的cpu使用情况,它表明只使用了1个核心。

由于列表理解没有副作用,因此同时测试多对数字没有危险。有没有办法让Haskell自动执行此操作,或者如果没有,我的代码如何修改才能执行此操作?

(编辑)运行print时出错(amicableNumberSums using parList):

Couldn't match type `a0 -> Eval a0' with `[Int]'
Expected type: Strategy [Int]
  Actual type: Strategy a0 -> Strategy [a0]
In the second argument of `using', namely `parList'
In the first argument of `print', namely
  `(amicableNumberSums `using` parList)'
In the expression: print (amicableNumberSums `using` parList)

(编辑)两种建议方法的表现:

Ørjan Johansen's method: 218.79s elapsed parallel (4 cores + 4 hyperthreading)
                         279.37s elapsed sequential (single core)
bheklilr's method: 247.82s elapsed parallel (4 cores + 4 hyperthreading)
                   274.10s elapsed sequential (single core)
Original method: 278.69s elapsed

这并不像我希望的那么大,但我现在对这个问题有了正确的答案,直到我学到了更多的Haskell,这已经足够了。

2 个答案:

答案 0 :(得分:20)

这是一个简单的例子:

simple = 1 : 1 : [a + b | a <- simple, b <- simple]

你会如何并行化?您如何将此概括为任何列表理解以确定它是否可以并行化?如果列表中的任何其他列表理解是无限的,为每个元素引发一个新线程将意味着引发无限的线程。如果由于线程开销过大而导致计算速度变慢而按顺序计算列表实际上要快得多,该怎么办?如果只需要列表的前10个元素怎么办?当需要一小部分时,贪婪地计算整个列表并不是很好。

相反,GHC选择向程序员提供关于何时以及如何并行化列表计算的权力。您可以选择使用Control.Parallel.Strategies模块完成的工作,而不是隐式为您做事:

print $ amicableNumberSums `using` parList rdeepseq

或者并行计算列表的块:

print $ amicableNumberSums `using` parListChunk 64 rdeepseq

请注意,您必须使用seq和co。在适当的时间将数据导入NF。

Control.Parallel.Strategies公开的API使您能够定义完全独立于数据结构本身甚至其他算法的并行计算纯数据结构的不同方法。这与大多数其他编程语言形成鲜明对比,这些编程语言迫使您将并行性与其他算法强烈耦合,甚至是如何构造结构。我强烈推荐阅读Simon Marlow的Parallel and Concurrent Haskell(它是免费在线的!),这比我解释它是如何工作以及如何使用它做得好得多。

答案 1 :(得分:5)

@ bheklilr的答案处理并行化策略的一般方法,但正如我在上面的注释中暗示的那样,原始列表理解的编写方式强制所有amicable测试在基于parList之前发生对它的策略可以得到它的元素并开始评估它们,所以我不认为@ bheklilr的代码对于这个特定情况会非常有效。

这是我对改进的建议。您需要重写列表推导,以便将工作划分为定义明确且独立的块,例如:通过组合中间列表中的x值。例如,它可以等效地编写为

concat [[ x+y | y <-[1..10000], (amicable x y)] | x <- [1..10000]]

现在,您可以在理解与最终concat之间建立并行评估策略:

concat ([[ x+y | y <-[1..10000], (amicable x y)] | x <- [1..10000]]
        `using` parList rdeepseq)

(我在这里使用rdeepseq,因为预先连接列表的元素也是列表,我们想要评估它们所有的整数元素。)