如何在我的haskell并行代码中利用任何并行性?

时间:2011-03-17 22:19:56

标签: haskell parallel-processing multicore

我刚刚说过使用GHC 6.12开发haskell半显式并行机制。我编写了下面的haskell代码来并行计算列表上4个元素的fibonnaci函数的映射,同时将函数sumEuler的映射计算在两个元素上。

import Control.Parallel
import Control.Parallel.Strategies

fib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)

mkList :: Int -> [Int]
mkList n = [1..n-1]

relprime :: Int -> Int -> Bool
relprime x y = gcd x y == 1

euler :: Int -> Int
euler n = length (filter (relprime n) (mkList n))

sumEuler :: Int -> Int
sumEuler = sum . (map euler) . mkList

-- parallel initiation of list walk                                                                                                                                    
mapFib :: [Int]
mapFib = map fib [37, 38, 39, 40]

mapEuler :: [Int]
mapEuler = map sumEuler [7600, 7600]

parMapFibEuler :: Int
parMapFibEuler = (forceList mapFib) `par` (forceList mapEuler `pseq` (sum mapFib + sum mapEuler))

-- how to evaluate in whnf form by forcing                                                                                                                                
forceList :: [a] -> ()
forceList [] = ()
forceList (x:xs) = x `pseq` (forceList xs)


main = do putStrLn (" sum : " ++ show parMapFibEuler)

并行改进我的程序我用 par pseq 以及强制函数重写了它以强制进行评估。我的问题是,通过查看threadscope,我似乎没有获得任何并行性。情况更糟,因为我没有获得任何加速。

Threadscope observation

这就是为什么我有两个问题

问题1 如何修改我的代码以利用任何并行性?

问题2 我如何编写程序以使用策略(parMap,parList,rdeepseq等等)?

战略的第一次改进

根据他的贡献

parMapFibEuler = (mapFib, mapEuler) `using` s `seq` (sum mapFib + sum mapEuler) where
    s = parTuple2 (seqList rseq) (seqList rseq)

并行性出现在线程范围内但不足以具有显着的加速

enter image description here

4 个答案:

答案 0 :(得分:7)

你没有看到任何并行性的原因是因为你的火花被垃圾收集了。使用+RTS -s运行程序并记下以下行:

  SPARKS: 1 (0 converted, 1 pruned)

火花已被“修剪”,这意味着被垃圾收集器移除。在GHC 7中,我们改变了火花的语义,这样如果程序的其余部分没有引用火花就会被垃圾收集(GC'd);详细信息位于the "Seq no more" paper

为什么火花GC会在您的情况下?看一下代码:

parMapFibEuler :: Int
parMapFibEuler = (forceList mapFib) `par` (forceList mapEuler `pseq` (sum mapFib + sum mapEuler))

这里的火花是表达式forkList mapFib。请注意,程序的其余部分不需要此表达式的值;它只作为par的参数出现。 GHC知道它不是必需的,因此它会被垃圾收集。

parallel套餐最近更改的重点是让你轻松避开这个熊陷阱。一个好的经验法则是直接使用Control.Parallel.Strategies而不是parpseq。我写这篇文章的首选方式是

parMapFibEuler :: Int
parMapFibEuler = runEval $ do
  a <- rpar $ sum mapFib
  b <- rseq $ sum mapEuler
  return (a+b)

但遗憾的是,这不适用于GHC 7.0.2,因为spark sum mapFib作为静态表达式(CAF)浮出来,并且运行时并不认为指向静态表达式的火花是值得保留(我会解决这个问题)。当然,这不会发生在真正的程序中!因此,让我们让程序更加真实,并打败CAF优化:

parMapFibEuler :: Int -> Int
parMapFibEuler n = runEval $ do
  a <- rpar $ sum (take n mapFib)
  b <- rseq $ sum (take n mapEuler)
  return (a+b)

main = do [n] <- fmap (fmap read) getArgs
          putStrLn (" sum : " ++ show (parMapFibEuler n))

现在我与GHC 7.0.2获得了良好的并行性。但请注意,@ John的评论也适用:通常你想要寻找更细粒度的并行性,以便让GHC使用你所有的处理器。

答案 1 :(得分:6)

你的并行性过于粗糙,无法产生很大的有益效果。可以有效并行完成的最大工作块位于sumEuler,因此您应该在其中添加par注释。尝试将sumEuler更改为:

sumEuler :: Int -> Int
sumEuler = sum . (parMap rseq euler) . mkList

parMap来自Control.Parallel.Strategies;它表示可以并行完成的地图。第一个参数rseq具有类型Strategy a,用于强制计算到特定点,否则由于懒惰而无法完成任何工作。 rseq适用于大多数数字类型。

在这里向fib添加并行性没有用,低于约fib 40,没有足够的工作来使其值得。

除了threadscope之外,使用-s标志运行程序也很有用。寻找像以下一样的行:

SPARKS: 15202 (15195 converted, 0 pruned)

在输出中。每个spark都是工作队列中的一个条目,可以并行执行。转换的火花实际上是并行完成的,而修剪的火花意味着主线程在工作线程有机会之前到达它们。如果修剪的数字很高,则意味着您的并行表达式太精细了。如果火花的总数很少,那么你并没有尝试并行做足够的事情。

最后,我认为parMapFibEuler更好地写成:

parMapFibEuler :: Int
parMapFibEuler = sum (mapFib `using` parList rseq) + sum mapEuler

mapEuler太短暂,无法在此处有效表达任何并行性,尤其是euler已经并行执行。我怀疑它对mapFib产生了重大影响。如果列表mapFibmapEuler更长,则此处的并行性将更有用。您可以使用parList代替parBuffer,而{{1}}往往适用于列表消费者。

使用GHC 7.0.2,进行这两项更改可以将运行时间从12秒减少到8秒。

答案 2 :(得分:1)

嗯......也许吧?

((forceList mapFib) `par` (forceList mapEuler)) `pseq` (sum mapFib + sum mapEuler)

即。在后台生成mapFib并计算mapEuler,并且仅在它(mapEuler)之后计算(+)的总和。 其实我猜你可以这样做:

parMapFibEuler = a `par` b `pseq` (a+b) where
     a = sum mapFib
     b = sum mapEuler

关于Q2: 据我所知,策略是将数据结构与parseq结合起来的“策略”。
您可以写下forceList = withStrategy (seqList rseq)
您也可以编写如下代码:

parMapFibEuler = (mapFib, mapEuler) `using` s `seq` (sum mapFib + sum mapEuler) where
    s = parTuple2 (seqList rseq) (seqList rseq)

即。应用于两个列表的元组的策略将强制它们并行进行评估,但每个列表将被强制按顺序进行评估。

答案 3 :(得分:1)

首先,我假设你知道你的fib定义很糟糕,你只是这样做才能使用并行包。

你似乎在错误的水平上寻求并行性。并行化mapFibmapEuler不会带来更好的加速,因为有更多的工作要计算mapFib。你应该做的是平行计算这些非常昂贵的元素,这些元素稍微好一点但不过分:

mapFib :: [Int]
mapFib = parMap rdeepseq fib [37, 38, 39, 40]

mapEuler :: [Int]
mapEuler = parMap  rdeepseq sumEuler [7600, 7600, 7600,7600]

parMapFibEuler :: Int
parMapFibEuler = sum a + sum b
  where
  a = mapFib
  b = mapEuler

另外,我最初在Control.Parallel上使用Control.Parallel.Strategies进行了战斗,但是它已经开始喜欢它了,因为它更具可读性并避免像你这样的问题,人们会期望并行性,并且不得不眯着眼睛看看它为什么你没有得到任何。

最后,您应该始终发布编译方式以及如何运行您期望并行化的代码。例如:

$ ghc --make -rtsopts -O2 -threaded so.hs -eventlog -fforce-recomp
[1 of 1] Compiling Main             ( so.hs, so.o )
Linking so ...
$ ./so +RTS -ls -N2
 sum : 299045675

收率: threadscope run with reasonable parallelism