为什么Haskell列表解析不是并行执行的?

iaqfqrcu  于 2023-01-26  发布在  其他
关注(0)|答案(3)|浏览(142)

我在做欧拉计划第21题的家庭作业,我有这样一个理解列表:

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

这需要很长的时间来执行(可以理解,因为它测试10000^2对数字),看看我的cpu使用情况,它显示只有1个内核正在使用。
由于对列表解析没有副作用,因此不存在同时测试多对数字的危险。有没有办法让Haskell自动做这件事,或者如果没有,怎么能修改我的代码来做这件事?
(编辑)运行打印时出错(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 这是足够的。

velaa5lx

velaa5lx1#

下面是一个简单的例子:

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(它在网上是免费的!),它在解释它如何工作和如何使用方面比我做得好得多。

rpppsulh

rpppsulh2#

@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是因为预连接列表的元素也是列表,我们希望计算它们所有的整数元素。)

lhcgjxsq

lhcgjxsq3#

OP报告的结果并不令人满意,我进一步挖掘了一下。事实证明,使用多核不仅需要其他答案中提到的显式并行代码,还需要特定的编译运行时标志。具体到ghc,它们分别是-threaded+RTS -N。有了这些设置,我在使用10个核的情况下获得了5倍的速度提升。
完整的可运行程序:

module Main where

import Control.Parallel.Strategies ( parList, rdeepseq, using )

onefactor :: Int -> Int -> Int
onefactor x y
  | x `rem` y == 0 = y
  | otherwise = 0

auxsumfactors :: Int -> Int -> Int
auxsumfactors _ 1 = 1
auxsumfactors x y = onefactor x y + auxsumfactors x (y-1)

sumfactors :: Int -> Int
sumfactors 1 = 1
sumfactors x = auxsumfactors x (x-1)

amicable :: Int -> Int -> Bool
amicable i j = i == sumfactors j && j == sumfactors i

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

main :: IO ()
main = putStrLn . show $ amicableNumberSums

注意,我将x的上限减少到1000是为了理智起见,并且我刚刚从here复制了helper函数。
结果(在我的苹果M1 Max上)是:

$ ghc parListComprehension.hs
$ time ./parListComprehension
[2,12,56,504,504,992]
./parListComprehension  1773.63s user 5.26s system 99% cpu 29:40.56 total
$ ghc -O2 parListComprehension.hs
$ time ./parListComprehension
[2,12,56,504,504,992]
./parListComprehension  31.97s user 0.07s system 98% cpu 32.411 total

因此,仅仅设置-O2就可以将运行时间提高55倍!根据GHC手册,-O2是最高级别的“方便包”优化和方法:
应用每一个不危险的优化,即使这意味着明显更长的编译时间。
现在我们添加并行性。
使用bheklilr答案中的示例代码,我无法逐字检测运行时行为的任何变化,所以我将这些结果省略。
相反,让我们直接使用Ørjan Johansen的方法,将amicableNumberSums改为:

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

如果我像以前那样编译和运行,结果不会有有意义的改变,这表明对concat的改变没有引入显著的开销,并且默认情况下,执行仍然绑定到单核上。要在多核上运行,ghc需要一些鼓励:

$ ghc -O2 -threaded parListComprehension.hs
$ time ./parListComprehension +RTS -N
[2,12,56,504,504,992]
./parListComprehension +RTS -N  57.20s user 0.20s system 940% cpu 6.103 total

因此,似乎有相当多的开销(几乎两倍的CPU时间),但通过投入10个核心,我们仍然得到了5倍的速度提高。
-threaded+RTS -N标志是紧密联系在一起的--你不能只拥有一个而不拥有另一个。+RTS标志表示后面的标志是用于“运行时系统”的,-N <x>指定要使用的“并发线程”的数量。如果未指定<x>,则运行时将自行选择基于有多少处理器在您的机器”。更多关于这些选项的信息在罚款手册。

相关问题