【问题标题】:How to exploit any parallelism in my haskell parallel code?如何利用我的 haskell 并行代码中的任何并行性?
【发布时间】:2011-07-17 18:55:11
【问题描述】:

我刚刚说过要在 GHC 6.12 中使用 haskell 半显式并行性。我编写了以下 haskell 代码来并行计算 fibonnaci 函数在列表中 4 个元素上的映射,同时计算 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)

为了并行改进我的程序,我用 parpseq 以及一个 forcing 函数来强制执行 whnf 评估。我的问题是,通过查看线程范围,我似乎没有获得任何并行性。情况更糟,因为我没有获得任何加速。

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

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

问题 2 如何编写程序以使用 Strategies(parMap、parList、rdeepseq 等...)?

策略的首次改进

根据他的贡献

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

并行性出现在线程范围中,但不足以显着加速

【问题讨论】:

  • 并行包在GHC 7中有很大的改进,你也可以考虑升级一下。
  • 你可以记住你的 fib 函数以获得一些加速...

标签: haskell parallel-processing multicore


【解决方案1】:

您在这里看不到任何并行性的原因是您的 spark 已被垃圾收集。使用+RTS -s 运行程序并注意这一行:

  SPARKS: 1 (0 converted, 1 pruned)

火花已被“修剪”,这意味着被垃圾收集器移除。在 GHC 7 中,我们对 spark 的语义进行了更改,如果程序的其余部分没有引用 spark,现在它会被垃圾回收(GC'd);详情在the "Seq no more" paper

为什么在你的情况下 spark GC'd?看代码:

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)浮动出来的,并且运行时认为指向静态表达式的 spark 不值得保留(我会解决这个问题)。当然,这不会在真正的程序中发生!因此,让我们让程序更真实一点,并击败 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 的 cmets 也适用:通常您希望寻找更细粒度的并行性,以便让 GHC 使用您的所有处理器。

【讨论】:

  • 非常感谢;它解释了我在查看此问题时想知道的一些行为。
【解决方案2】:

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

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

parMap 来自Control.Parallel.Strategies;它表达了一个可以并行完成的映射。第一个参数,rseq,类型为Strategy a,用于强制计算到特定点,否则由于懒惰,不会做任何工作。 rseq 适用于大多数数字类型。

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

除了线程作用域之外,使用-s 标志运行您的程序也很有用。寻找这样的一行:

SPARKS: 15202 (15195 converted, 0 pruned)

在输出中。每个火花都是可能并行执行的工作队列中的一个条目。转换的火花实际上是并行完成的,而修剪的火花意味着主线程在工作线程有机会这样做之前就已经到达它们。如果修剪的数量很大,则意味着您的并行表达式过于细粒度。如果火花的总数很低,则说明您没有尝试并行完成足够的工作。

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

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

mapEuler 太短了,无法在这里有效地表达任何并行性,尤其是euler 已经并行执行。我怀疑它是否对mapFib 产生了重大影响。如果列表 mapFibmapEuler 更长,这里的并行性会更有用。您可以使用parBuffer 而不是parList,这对于列表使用者来说效果很好。

使用 GHC 7.0.2 对我来说,进行这两项更改将运行时间从 12 秒缩短到 8 秒。

【讨论】:

    【解决方案3】:

    首先,我假设您知道您的 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.Strategies 而不是 Control.Parallel,但后来喜欢它,因为它更具可读性并且避免了像您这样的问题,即人们期望并行性并且不得不眯着眼看它以找出您的原因什么都没有。

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

    $ 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
    

    产量:

    【讨论】:

      【解决方案4】:

      嗯……也许吧?

      ((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
      

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

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

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

      【讨论】:

      • 感谢回复,但您提出的代码与我在问题中写的代码相似,我已经测试过您的命题和线程范围绘图与以前相同
      • 只需稍作修改即可使其工作 parMapFibEuler = ((mapFib, mapEuler) using s) seq (sum mapFib + sum mapEuler) 其中 s = parTuple2 (seqList rseq) ( seqList rseq)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-09-02
      • 1970-01-01
      • 2019-12-18
      • 2020-09-17
      • 2020-11-15
      • 1970-01-01
      相关资源
      最近更新 更多