理解Haskell并行Fibonacci结果

理解Haskell并行Fibonacci结果,haskell,parallel-processing,Haskell,Parallel Processing,鉴于: (2)-拆分列表+在每一半上使用rpar Tot time (elapsed) Avg pause Max pause Gen 0 0 colls, 0 par 0.000s 0.000s 0.0000s 0.0000s Gen 1 2 colls, 1 par 0.000s 0.000s 0.0001s 0.0001s Parallel GC work balance

鉴于:

(2)-拆分列表+在每一半上使用
rpar

 Tot time (elapsed)  Avg pause  Max pause
  Gen  0         0 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s
  Gen  1         2 colls,     1 par    0.000s   0.000s     0.0001s    0.0001s

  Parallel GC work balance: 84.39% (serial 0%, perfect 100%)

  TASKS: 4 (1 bound, 3 peak workers (3 total), using -N2)

  SPARKS: 80 (74 converted, 0 overflowed, 0 dud, 0 GC'd, 6 fizzled)

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time    8.594s  (  4.331s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.000s elapsed)
  Total   time    8.594s  (  4.332s elapsed)

  Alloc rate    12,259 bytes per MUT second

  Productivity 100.0% of total user, 198.4% of total elapsed

根据我对文本的理解,为什么
parMap
版本比split-up+
rpar
版本快?

我不确定接下来的内容是否是影响计时的唯一因素,但它肯定起到了很大的作用

对于列表,像这样拆分工作效率很低

请记住,并行执行只需要很少的工作,因此开始为列表中的每个元素执行某些操作的最快方法就是一个接一个地运行它们,并使用
rpar
激发它们。这就是
parMap
所做的

在您的例子中,
splitAt
需要做更多的工作:它需要遍历列表的一半,然后为另一个列表分配空间。您还可以在该遍历过程中触发
fib
执行


要了解我的意思,请尝试将
[1..40]
替换为
(复制1000 35)
。这是更可并行化的:许多相当困难的问题,都是同样的困难。对于1000个元素的长列表,
splitIt
运行时间超过100秒,而
spark
运行时间不到1秒您的解决方案最终花费绝大多数时间拆分和追加列表,而不是计算任何内容。

首先请注意,计算fib n所需的工作量是指数型的。这意味着计算
map fib[1..n]
所需的时间与计算
fib(n+1)
所需的时间大致相同。要查看这一点,只需打印出计算
fib n
的各种
n
值所需的时间:

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0         0 colls,     0 par    0.000s   0.000s     0.0000s    0.0000s
  Gen  1         2 colls,     1 par    0.000s   0.000s     0.0002s    0.0003s

  Parallel GC work balance: 12.41% (serial 0%, perfect 100%)

  TASKS: 4 (1 bound, 3 peak workers (3 total), using -N2)

  SPARKS: 40 (10 converted, 0 overflowed, 0 dud, 0 GC'd, 30 fizzled)

  INIT    time    0.000s  (  0.001s elapsed)
  MUT     time    7.453s  (  3.751s elapsed)
  GC      time    0.000s  (  0.000s elapsed)
  EXIT    time    0.000s  (  0.000s elapsed)
  Total   time    7.453s  (  3.752s elapsed)

  Alloc rate    14,398 bytes per MUT second

  Productivity 100.0% of total user, 198.6% of total elapsed
要使用两个线程高效地计算
map fib[1..40]
,您需要尽可能均衡每个线程完成的工作量。事实证明,这样一种工作非常好的分工是一个线程compute
映射fib[1..38]
,另一个线程compute
[fib 39,fib 40]

如果为每个
fib i
计算创建一个火花,那么两个线程之间的分工是完全不确定的。为了平衡每根线所做的功,你实际上需要仔细设计火花是什么

现在看看你的两个程序中产生的火花数量——一个80,另一个40。很明显,每个
fibi
都会被触发,这意味着在这两种情况下,
fibi
计算都会被随机分配到两个线程

以下是一种通过两个线程获得约1.5加速的方法:

import System.TimeIt
import Control.Monad
...
main = forM_ [1..40] $ \n -> timeIt $ print (fib n)
导入控制.Parallel.Strategies
fib::Int->Int
fib x

|谢谢你的详细回答,埃里克。就我个人的理解而言,在
parMap rpar fib
中,
parMap
的第一个合理参数是什么?一般来说,我建议始终使用
parMap rdeepseq…
import System.TimeIt
import Control.Monad
...
main = forM_ [1..40] $ \n -> timeIt $ print (fib n)
import Control.Parallel.Strategies

fib :: Int -> Int
fib x 
 | x <= 1    = 1
 | otherwise = fib (x-1) + fib (x-2)

main = do
  let fs = (map fib [1..40]) `using` parListSplitAt 38 rdeepseq rdeepseq
  print fs