如何用Haskell向量编写并行代码?

时间:2010-11-15 17:04:43

标签: haskell vector parallel-processing

一方面,在Haskell中Vector a似乎是用作数组数组的首选类型。甚至有一个(不完整的)Vector Tutorial

另一方面,Control.Parallel.Strategies主要根据Traversable来定义。矢量库不提供这些实例。

Traversable t的最小完整定义还应定义Foldable

traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
sequenceA :: Applicative f => t (f a) -> f (t a)

我看不出如何为sequenceA定义Data.Vector.Unboxed.Vector。那么,使用未装箱的向量编写并行代码的最佳方法是什么?定义一些新的临时策略,例如evalVector或明确使用parpseq或使用普通Data.Array代替向量?

P.S。普通Array可以并行化而没有问题:https://gist.github.com/701888

2 个答案:

答案 0 :(得分:6)

这是parVector的黑客工作,但这对我有用:

import qualified Data.Vector as V
import Control.Parallel.Strategies
import Control.Parallel
import Control.DeepSeq

ack :: Int -> Int -> Int
ack 0 n = n+1
ack m 0 = ack (m-1) 1
ack m n = ack (m-1) (ack m (n-1))

main = do
  let vec = V.enumFromN 1 1000
  let res = (V.map (ack 2) vec) `using` parVector
  print res

parVector :: NFData a => Strategy (V.Vector a)
parVector vec = eval vec `seq` Done vec
  where
  chunkSize = 1
  eval v
    | vLen == 0 = ()
    | vLen <= chunkSize = rnf (v V.! 0) -- FIX this to handle chunks > 1
    | otherwise = eval (V.take half v) `par` eval (V.drop half v)
    where vLen = V.length v
          half = vLen `div` 2

运行此代码:

[tommd@Mavlo Test]$ ghc --make -O2 -threaded t.hs
... dumb warning ...
[tommd@Mavlo Test]$ time ./t +RTS -N1 >/dev/null
real    0m1.962s user    0m1.951s sys     0m0.009s
[tommd@Mavlo Test]$ time ./t +RTS -N2 >/dev/null
real    0m1.119s user    0m2.221s sys 0m0.005s

当我在类型签名中使用Integer代替Int运行代码时:

[tommd@Mavlo Test]$ time ./t +RTS -N2 >/dev/null

real    0m4.754s
user    0m9.435s
sys     0m0.028s
[tommd@Mavlo Test]$ time ./t +RTS -N1 >/dev/null

real    0m9.008s
user    0m8.952s
sys     0m0.029s

摇滚!

编辑:一个更接近你早期尝试的解决方案更清晰(它不使用来自三个独立模块的功能)并且效果很好:

parVector :: NFData a => Strategy (V.Vector a)
parVector vec =
  let vLen = V.length vec
      half = vLen `div` 2
      minChunk = 10
  in  if vLen > minChunk
      then do
        let v1 = V.unsafeSlice 0 half vec
            v2 = V.unsafeSlice half (vLen - half) vec
        parVector v1
        parVector v2
        return vec
      else
        evalChunk (vLen-1) >>
        return vec
  where
  evalChunk 0 = rpar (rdeepseq (vec V.! 0)) >> return vec
  evalChunk i = rpar (rdeepseq (vec V.! i)) >> evalChunk (i-1)

从这个解决方案中学到的东西:

  1. 它使用Eval monad,这是严格的,所以我们肯定会引发一切(与在let中包装并记住使用爆炸模式相比)。
  2. 与你提议的实现相反,它(a)不构造一个新的向量,这是昂贵的(b)evalChunk强制使用rparrdeepseq评估每个元素(I不要相信rpar vec强制任何向量的元素。)
  3. 与我的观点相反,slice采用起始索引和长度,而不是起始和结束索引。糟糕!
  4. 我们仍需要导入Control.DeepSeq (NFData),但我已通过电子邮件发送图书馆列表以尝试解决该问题。
  5. 性能与此答案中的第一个parVector解决方案类似,因此我不会发布数字。

答案 1 :(得分:2)

1)正如您可能知道的那样,vectorDPH作品的产物,其证明比研究人员最初的预期更难。

2)未装箱的向量不能将多个CPU的各个元素的工作分开。

3)我对盒装载体更有希望。类似的东西:

using (map (rnf . (vec !)) [0..V.length vec - 1]) (parList rdeepseq)

或许你可以避免构建列表并使用parlist。我认为只分配数组的部分就足够了。下面的代码可能已经破解了,但是使用parVector创建自己的rnf并将向量分成两半直到它是单个元素(或一些可调整的元素块大小)的概念应该有效。< / p>

parVector :: Strategy (Vector a)
parVector = let !_ = eval vec in Done vec
  where
  chunkSize = 1
  eval v
    | vLen == 0 = ()
    | vLen <= chunkSize = rnf (v ! 0) -- FIX this to handle chunks > 1
    | otherwise = eval (V.take half v) `par` eval (V.drop half v)
    where vLen = V.length v
          half = vLen `div` 2