Ich bin ziemlich neu in Haskell Threads (und parallele Programmierung im Allgemeinen) und ich bin mir nicht sicher, warum meine parallele Version eines Algorithmus langsamer als die entsprechende sequentielle Version läuft.Haskell: Paralleler Code ist langsamer als sequentielle Version
Der Algorithmus versucht, alle K-Kombinationen ohne Rekursion zu finden. Dazu verwende ich this Helferfunktion, die eine Zahl mit k Bits gegeben, gibt die nächste Nummer mit der gleichen Anzahl von Bits gesetzt:
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
Es ist nun leicht sequentiell alle k-Kombinationen zu erhalten, in der Bereich [(2^k - 1), (2^(nk) + ... + 2^(n-1)):
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)
Meine Idee ist, dass dieser Bereich dieses leicht sein sollte parallelisierbaren Splitting in kleinere Teile. Zum Beispiel:
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)])
An dieser Stelle möchte Ich mag jede Spaltung parallel laufen zu lassen, so etwas wie:
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
für pallalelization Ich bin mit dem monad-par
Paket:
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
Das Ergebnis ist, dass die sequenzielle Version viel schneller ist und wenig Speicher benötigt, während die parallele Version viel Speicher verbraucht und sich langsamer bemerkbar macht.
Was könnten die Gründe dafür sein? Sind Threads ein guter Ansatz für dieses Problem? Zum Beispiel erzeugt jeder Thread eine (möglicherweise große) Liste von ganzen Zahlen und der Haupt-Thread reduziert die Ergebnisse; Sind Threads, von denen erwartet wird, dass sie viel Speicher benötigen oder einfach dazu gedacht sind, einfache Ergebnisse zu erzeugen (d. h. nur CPU-intensive Berechnungen)?
Ich kompiliere mein Programm mit stack build --ghc-options -threaded --ghc-options -rtsopts --executable-profiling --library-profiling
und führe es mit ./.stack-work/install/x86_64-osx/lts-6.1/7.10.3/bin/combinatorics 20 3 4 +RTS -pa -N4 -RTS
für n = 20, k = 3 und numSplits = 4. Ein Beispiel des Profiling-Reports für die parallele Version finden Sie unter here und für die sequentielle Version here.
Wie werden Sie laufen und das Programm zu testen? – pdexter
Haben Sie einen Parameter 'k' in der' Kämme' Funktion vergessen? – user640078
@pdexter habe gerade meine Frage mit build/run-Befehlen und Profiling-Berichten für parallele und sequenzielle Versionen aktualisiert. – jarandaf