Почему мой параллельный код еще медленнее, чем без параллелизма?

#haskell

Вопрос:

Я следовал книге Саймона Марлоу о параллельном Хаскелле (глава 1), используя rpar / rseq .

Ниже приведен код (решение симуляции моста в игре Squid):

 {-# LANGUAGE FlexibleContexts #-}  import Control.DeepSeq (force) import Control.Exception (evaluate) import Control.Parallel.Strategies import Data.Array.IO  ( IOUArray,  getAssocs,  newListArray,  readArray,  writeArray,  ) import Data.Functor ((lt;amp;gt;)) import System.Environment (getArgs) import System.Random (randomRIO)  game ::  Int -gt; -- number of steps  Int -gt; -- number of glass at each step  Int -gt; -- number of players  IO Int -- return the number of survivors game totalStep totalGlass = go 1 totalGlass  where  go currentStep currentGlass numSurvivors  | numSurvivors == 0 || currentStep gt; totalStep = return numSurvivors  | otherwise = do  r lt;- randomRIO (1, currentGlass)  if r == 1  then go (currentStep   1) totalGlass numSurvivors  else go currentStep (currentGlass - 1) (numSurvivors - 1)  simulate :: Int -gt; IO Int -gt; IO [(Int, Int)] simulate n game =  (newListArray (0, 16) (replicate 17 0) :: IO (IOUArray Int Int))  gt;gt;= go 1  gt;gt;= getAssocs  where  go i marr  | i lt;= n = do  r lt;- game  readArray marr r gt;gt;= writeArray marr r . (  1)  go (i   1) marr  | otherwise = return marr  main1 :: IO () main1 = do  [n, steps, glassNum, playNum] lt;- getArgs lt;amp;gt; Prelude.map read  res lt;- simulate n (game steps glassNum playNum)  mapM_ print res  main2 :: IO () main2 = do  putStrLn "Running main2"  [n, steps, glassNum, playNum] lt;- getArgs lt;amp;gt; Prelude.map read  res lt;- runEval $ do  r1 lt;- rpar $ simulate (div n 2) (game steps glassNum playNum) gt;gt;= evaluate . force  r2 lt;- rpar $ simulate (div n 2) (game steps glassNum playNum) gt;gt;= evaluate . force  rseq r1  rseq r2  return $  (l1 l2 -gt; zipWith (e1 e2 -gt; (fst e1, snd e1   snd e2)) l1 l2)  lt;$gt; r1  lt;*gt; r2   mapM_ print res  main = main2  

Для main2 я скомпилировал с помощью:

 ghc -O2 -threaded ./squid.hs  

и бежать, как:

 ./squid 10000000 18 2 16  RTS -N2  

Я не могу понять, почему main1 это быстрее, чем main2 в то время main2 как в этом есть параллелизм.

Может ли кто-нибудь дать мне несколько комментариев по моему коду относительно того, правильно ли это использовать параллелизм?

Обновление: Вот обновленная версия (новая random довольно громоздка в использовании):

 {-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RankNTypes #-}  import Control.Monad.ST (ST, runST) import Control.Parallel.Strategies (rpar, rseq, runEval) import Data.Array.ST  ( STUArray,  getAssocs,  newListArray,  readArray,  writeArray,  ) import Data.Functor ((lt;amp;gt;)) import System.Environment (getArgs) import System.Random (StdGen) import System.Random.Stateful  ( StdGen,  applySTGen,  mkStdGen,  runSTGen,  uniformR,  )  game ::  Int -gt; -- number of steps  Int -gt; -- number of glass at each step  Int -gt; -- number of players  StdGen -gt;  ST s (Int, StdGen) -- return the number of survivors game ns ng = go 1 ng  where  go  !cs -- current step number  !cg -- current glass number  !ns -- number of survivors  !pg -- pure generator  | ns == 0 || cs gt; ns = return (ns, pg)  | otherwise = do  let (r, g') = runSTGen pg (applySTGen (uniformR (1, cg)))  if r == 1  then go (cs   1) ng ns g'  else go cs (cg - 1) (ns - 1) g'  simulate :: Int -gt; (forall s. StdGen -gt; ST s (Int, StdGen)) -gt; [(Int, Int)] simulate n game =  runST $  (newListArray (0, 16) (replicate 17 0) :: ST s1 (STUArray s1 Int Int))  gt;gt;= go 1 (mkStdGen n)  gt;gt;= getAssocs  where  go !i !g !marr  | i lt;= n = do  (r, g') lt;- game g  readArray marr r gt;gt;= writeArray marr r . (  1)  go (i   1) g' marr  | otherwise = return marr  main :: IO () main = do  [n, steps, glassNum, playNum] lt;- getArgs lt;amp;gt; Prelude.map read  let res = runEval $ do  r1 lt;- rpar $ simulate (div n 2 - 1) (game steps glassNum playNum)  r2 lt;- rpar $ simulate (div n 2   1) (game steps glassNum playNum)  rseq r1  rseq r2  return $ zipWith (e1 e2 -gt; (fst e1, snd e1   snd e2)) r1 r2  mapM_ print res  

Обновление 2:

Используйте чистый код, и затраченное время сократится до 7 секунд.

 {-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE RankNTypes #-}  import Control.Monad.ST ( runST, ST ) import Control.Parallel ( par, pseq ) import Data.Array.ST  ( getAssocs, newListArray, readArray, writeArray, STUArray ) import Data.Functor ((lt;amp;gt;)) import System.Environment (getArgs) import System.Random (StdGen, uniformR, mkStdGen) game ::  Int -gt; -- number of total steps  Int -gt; -- number of glass at each step  Int -gt; -- number of players  StdGen -gt;  (Int, StdGen) -- return the number of survivors game ts ng = go 1 ng  where  go  !cs -- current step number  !cg -- current glass number  !ns -- number of survivors  !pg -- pure generator  | ns == 0 || cs gt; ts = (ns, pg)  | otherwise = do  let (r, g') = uniformR (1, cg) pg  if r == 1  then go (cs   1) ng ns g'  else go cs (cg - 1) (ns - 1) g'  simulate :: Int -gt; (StdGen -gt; (Int, StdGen)) -gt; [(Int, Int)] simulate n game =  runST $  (newListArray (0, 16) (replicate 17 0) :: ST s1 (STUArray s1 Int Int))  gt;gt;= go 1 (mkStdGen n)  gt;gt;= getAssocs  where  go !i !g !marr  | i lt;= n = do  let (r, g') = game g  readArray marr r gt;gt;= writeArray marr r . (  1)  go (i   1) g' marr  | otherwise = return marr  main :: IO () main = do  [n, steps, glassNum, playNum] lt;- getArgs lt;amp;gt; Prelude.map read   let r1 = simulate (div n 2 - 1) (game steps glassNum playNum)  r2 = simulate (div n 2   1) (game steps glassNum playNum)  res = zipWith (e1 e2 -gt; (fst e1, snd e1   snd e2)) r1 r2   res' = par r1 (pseq r2 res)   mapM_ print res'  

Комментарии:

1. Я не вижу никаких веских причин использовать ST генератор на основе этого. Вы можете использовать StateGenM StdGen с StateT StdGen (ST s) , что должно быть быстрее. Или вы можете передавать чистые генераторы вручную, что кажется немного раздражающим.

2. @dfeuer спасибо, теперь намного быстрее.

3. @dfeuer я на самом деле использовал чистый генератор и переключил шаблон rpar/rseq на par/pseq

4. par является своего рода неофициально устаревшим, потому что системе времени выполнения трудно определить, когда результат все еще необходим. Я бы не стал этим пользоваться.

5. @dfeuer, это сбивает с толку, потому что я видел par в документации последнего руководства ghc, и они используют его в качестве своего рода основного примера…

Ответ №1:

На самом деле вы не используете никакого параллелизма. Ты пишешь

 r1 lt;- rpar $ simulate (div n 2) (game steps glassNum playNum) gt;gt;= evaluate . force  

Это запускает поток для оценки IO действия, а не для его выполнения. Это бесполезно.

Поскольку ваш simulate по сути чистый, вы должны преобразовать его из IO в ST s , поменяв местами соответствующие типы массивов и т. Д. Тогда вы сможете rpar (runST $ simulate ...) и на самом деле работать параллельно. Я не думаю force , что вызовы полезны/уместны в контексте; они освободят массивы раньше, но со значительными затратами.

Комментарии:

1. Я сделал то, что вы предложили, и действительно, это быстрее. Но-N2 все еще медленнее, чем-N1. Это нормально? Кроме того, если я просто запущу его как ./myprog без каких-либо RTS, будет ли программа запускаться автоматически параллельно?

2. @McBearHolden, часто приходится много работать над тем, чтобы параллельный код действительно выигрывал. В этом случае я подозреваю, что вы на самом деле не выполняете достаточно интересной работы в каждом потоке по сравнению с количеством, которое вы делаете, чтобы объединить результаты.