Haskell:并行代码比顺序版

时间:2016-06-10 09:52:34

标签: multithreading haskell parallel-processing

我对Haskell线程(以及一般的并行编程)很陌生,我不确定为什么我的并行版本的算法比相应的顺序版本运行得慢。

该算法尝试在不使用递归的情况下查找所有k组合。为此,我使用this辅助函数,它给出了一个设置了k位的数字,返回下一个具有相同位数的数字:

import Data.Bits    

nextKBitNumber :: Integer -> Integer
nextKBitNumber n
  | n == 0      = 0
  | otherwise   = ripple .|. ones
                       where smallest     = n .&. (-n)
                             ripple       = n + smallest
                             newSmallest  = ripple .&. (-ripple)
                             ones         = (newSmallest `div` smallest) `shiftR` 1 - 1

现在很容易顺序获得[(2 ^ k - 1),(2 ^(n-k)+ ... + 2 ^(n-1))范围内的所有k组合:

import qualified Data.Stream as ST

combs :: Int -> Int -> [Integer]
combs n k = ST.takeWhile (<= end) $ kBitNumbers start
  where start = 2^k - 1
        end   = sum $ fmap (2^) [n-k..n-1]

kBitNumbers :: Integer -> ST.Stream Integer
kBitNumbers = ST.iterate nextKBitNumber

main :: IO ()
main = do
  params <- getArgs
  let n = read $ params !! 0
      k = read $ params !! 1
  print $ length (combs n k)

我的想法是,这应该很容易并行化,将这个范围分成更小的部分。例如:

start :: Int -> Integer
start k = 2 ^ k - 1

end :: Int -> Int -> Integer
end n k = sum $ fmap (2 ^) [n-k..n-1]

splits :: Int -> Int -> Int -> [(Integer, Integer, Int)]
splits n k numSplits = fixedRanges ranges []
  where s                       = start k
        e                       = end n k
        step                    = (e-s) `div` (min (e-s) (toInteger numSplits))
        initSplits              = [s,s+step..e]
        ranges                  = zip initSplits (tail initSplits)
        fixedRanges [] acc      = acc
        fixedRanges [x] acc     = acc ++ [(fst x, e, k)]
        fixedRanges (x:xs) acc  = fixedRanges xs (acc ++ [(fst x, snd x, k)])

此时,我想并行运行每个分割,例如:

runSplit :: (Integer, Integer, Int) -> [Integer]
runSplit (start, end, k) = ST.takeWhile (<= end) $ kBitNumbers (fixStart start)
  where fixStart s
                   | popCount s == k = s
                   | otherwise       = fixStart $ s + 1

对于pallalelization我正在使用monad-par包:

import Control.Monad.Par
import System.Environment
import qualified Data.Set as S

main :: IO ()
main = do
  params <- getArgs
  let n               = read $ params !! 0
      k               = read $ params !! 1
      numTasks        = read $ params !! 2
      batches         = runPar $ parMap runSplit (splits n k numTasks)
      reducedNumbers  = foldl S.union S.empty $ fmap S.fromList batches
  print $ S.size reducedNumbers

结果是顺序版本更快,并且它使用的内存很少,而并行版本消耗大量内存并且明显变慢。

造成这种情况的原因可能是什么?线程是解决这个问题的好方法吗?例如,每个线程生成一个(可能很大的)整数列表,主线程会减少结果;是预期需要大量内存的线程还是仅仅意味着产生简单的结果(即只有cpu密集型计算)?

我使用stack build --ghc-options -threaded --ghc-options -rtsopts --executable-profiling --library-profiling编译我的程序并使用./.stack-work/install/x86_64-osx/lts-6.1/7.10.3/bin/combinatorics 20 3 4 +RTS -pa -N4 -RTS运行它,n = 20,k = 3和numSplits = 4。可以找到并行版本的分析报告示例here和顺序版here

2 个答案:

答案 0 :(得分:2)

在你的顺序版本中,调用combs不会在内存中建立一个列表,因为length消耗了一个元素后不再需要它并被释放。实际上,GHC甚至可能不为它分配存储空间。

例如,这需要一段时间,但不会占用大量内存:

main = print $ length [1..1000000000]  -- 1 billion

在您的并行版本中,您将生成子列表,将它们连接在一起,构建集等,因此每个子任务的结果必须保存在内存中。

更公平的比较是让每个并行任务计算其指定范围内的k位数的length,然后将结果相加。这样,每个并行任务找到的k位数字就不必保存在内存中,并且更像是顺序版本。

<强>更新

以下是如何使用parMap的示例。 注意:在7.10.2之下,我已经取得了不同的成功,并没有发现并行性 - 有时它会发生,有时它不会。(想出来 - 我使用的是-RTS -N2而不是+RTS -N2。)

{-
compile with: ghc -O2 -threaded -rtsopts foo.hs

compare:

  time ./foo 26 +RTS -N1
  time ./foo 26 +RTS -N2
-}

import Data.Bits    
import Control.Parallel.Strategies
import System.Environment

nextKBitNumber :: Integer -> Integer
nextKBitNumber n
  | n == 0      = 0
  | otherwise   = ripple .|. ones
                       where smallest     = n .&. (-n)
                             ripple       = n + smallest
                             newSmallest  = ripple .&. (-ripple)
                             ones         = (newSmallest `div` smallest) `shiftR` 1 - 1

combs :: Int -> Int -> [Integer]
combs n k = takeWhile (<= end) $ iterate nextKBitNumber start
  where start = 2^k - 1
        end   = shift start (n-k)

main :: IO ()
main = do
  ( arg1 : _) <- getArgs
  let n = read arg1
  print $ parMap rseq (length . combs n) [1..n]

答案 1 :(得分:0)

  

解决此问题的好方法

这个问题是什么意思?如果它是如何编写,分析和调整并行Haskell程序,那么这是必读的背景知识:

Simon Marlow:Haskell中的并行和并发编程 http://community.haskell.org/~simonmar/pcph/

特别是第15节(调试,调整,......)

使用threadscope! (由格拉斯哥Haskell编译器生成的线程配置文件信息的图形查看器)https://hackage.haskell.org/package/threadscope