我对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。
答案 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