2013-01-31 16 views
15

Ich versuche, mich um parallele Strategien zu kümmern. Ich denke, ich verstehe, was jeder der Kombinierer macht, aber jedes Mal, wenn ich versuche, sie mit mehr als einem Kern zu verwenden, verlangsamt sich das Programm beträchtlich.Effiziente parallele Strategien

Zum Beispiel eine Weile zurück Ich habe versucht, Histogramme (und davon eindeutige Wörter) aus ~ 700 Dokumente zu berechnen. Ich dachte, dass die Granularität auf Dateiebene in Ordnung wäre. Mit -N4 bekomme ich 1,70 Work Balance. Mit -N1 läuft es jedoch in der Hälfte der Zeit als mit -N4. Ich bin nicht sicher, was die Frage wirklich ist, aber ich würde gerne wissen, wie man entscheidet, wo/wann/wie man parallelisiert und etwas Verständnis dafür bekommt. Wie würde dies parallelisiert werden, so dass die Geschwindigkeit mit Kernen zunimmt anstatt zu sinken?

import Data.Map (Map) 
import qualified Data.Map as M 
import System.Directory 
import Control.Applicative 
import Data.Vector (Vector) 
import qualified Data.Vector as V 
import qualified Data.Text as T 
import qualified Data.Text.IO as TI 
import Data.Text (Text) 
import System.FilePath ((</>)) 
import Control.Parallel.Strategies 
import qualified Data.Set as S 
import Data.Set (Set) 
import GHC.Conc (pseq, numCapabilities) 
import Data.List (foldl') 

mapReduce stratm m stratr r xs = let 
    mapped = parMap stratm m xs 
    reduced = r mapped `using` stratr 
    in mapped `pseq` reduced 

type Histogram = Map Text Int 

rootDir = "/home/masse/Documents/text_conversion/" 

finnishStop = ["minä", "sinä", "hän", "kuitenkin", "jälkeen", "mukaanlukien", "koska", "mutta", "jos", "kuitenkin", "kun", "kunnes", "sanoo", "sanoi", "sanoa", "miksi", "vielä", "sinun"] 
englishStop = ["a","able","about","across","after","all","almost","also","am","among","an","and","any","are","as","at","be","because","been","but","by","can","cannot","could","dear","did","do","does","either","else","ever","every","for","from","get","got","had","has","have","he","her","hers","him","his","how","however","i","if","in","into","is","it","its","just","least","let","like","likely","may","me","might","most","must","my","neither","no","nor","not","of","off","often","on","only","or","other","our","own","rather","said","say","says","she","should","since","so","some","than","that","the","their","them","then","there","these","they","this","tis","to","too","twas","us","wants","was","we","were","what","when","where","which","while","who","whom","why","will","with","would","yet","you","your"] 
isStopWord :: Text -> Bool 
isStopWord x = x `elem` (finnishStop ++ englishStop) 

textFiles :: IO [FilePath] 
textFiles = map (rootDir </>) . filter (not . meta) <$> getDirectoryContents rootDir 
    where meta "." = True 
     meta ".." = True 
     meta _ = False 

histogram :: Text -> Histogram 
histogram = foldr (\k -> M.insertWith' (+) k 1) M.empty . filter (not . isStopWord) . T.words 

wordList = do 
    files <- mapM TI.readFile =<< textFiles 
    return $ mapReduce rseq histogram rseq reduce files 
    where 
    reduce = M.unions 

main = do 
    list <- wordList 
    print $ M.size list 

Wie für die Textdateien, ich bin mit pdfs umgewandelt in Text-Dateien, damit ich sie nicht bieten kann, aber für den Zweck, fast jedes Buch/Bücher von Projekt Gutenberg tun sollen.

bearbeiten: hinzugefügt Importe Skript

+1

'Histogramm = foldr (\ k -> M.insertWith '(+) k 1) M.leer. filter (nicht. isStopWord). T.words' sollte eher eine 'foldl' verwenden. Der 'foldr' erstellt ein Thunk so tief wie die Liste ist lange bevor es beginnen kann, die' Map' zu erstellen. –

+3

Es wäre viel einfacher, eine solche Frage zu beantworten, wenn Sie ein kleines und vollständiges Beispiel liefern würden. Ohne genau ins Detail zu schauen: Sind Sie sicher, dass 'rseq' als erster arg von' mapReduce' genug ist, um zu erzwingen, dass jeder Teil der Arbeit wirklich parallel gemacht wird? Ist der Arbeitsaufwand pro Listenelement in 'parMap' groß genug, um eine gute Granularität der parallelen Aufgaben zu gewährleisten? Haben Sie versucht, threadscope in Ihrem Programm auszuführen, um zu sehen, was auf jedem Kern vor sich geht? Haben Sie versucht, mit '+ RTS -s' zu laufen, um zu sehen, wie viel Zeit in der Garbage Collection verbracht wird? – kosmikus

+0

kosmikus, was für ein komplettes Beispiel meinst du? Abgesehen von Importen ist das Skript vollständig lauffähig. Für rseq/rdeepseq, versuchte ich mit anderen Kombinationen ohne Glück. Wie für ParMap, habe ich auch versucht, mit ParListChunk und ParListN Karte. Und was den threadscope anbelangt, schien es sowohl Action als auch GC zu geben. -s sagte, dass es 60% Arbeitszeit war, was besser war als der Fall -N1. – Masse

Antwort

4

In der Praxis kann es schwierig sein, die parallelen Kombinatoren gut skalieren zu lassen. Andere haben erwähnt, Ihren Code strenger zu machen, um sicherzustellen, dass Sie tatsächlich die Arbeit parallel tun, die auf jeden Fall wichtig ist .

Zwei Dinge, die Leistung wirklich töten können, sind viele Speicher Traversal und Speicherbereinigungen. Selbst wenn Sie nicht viel Müll produzieren, viele Speicher Traversal mehr Druck auf die CPU-Cache und schließlich Ihr Speicher-Bus wird der Flaschenhals. Ihre isStopWord Funktion führt eine Menge von String-Vergleichen durch und muss dafür eine ziemlich lange verkettete Liste durchlaufen. Sie können eine Menge Arbeit mit dem eingebauten Set Typ oder, noch besser, HashSet Typ aus dem unordered-containers Paket (da wiederholte Zeichenfolge Vergleiche können teuer sein, vor allem, wenn sie Commons Präfixe teilen).

import   Data.HashSet    (HashSet) 
import qualified Data.HashSet    as S 

... 

finnishStop :: [Text] 
finnishStop = ["minä", "sinä", "hän", "kuitenkin", "jälkeen", "mukaanlukien", "koska", "mutta", "jos", "kuitenkin", "kun", "kunnes", "sanoo", "sanoi", "sanoa", "miksi", "vielä", "sinun"] 
englishStop :: [Text] 
englishStop = ["a","able","about","across","after","all","almost","also","am","among","an","and","any","are","as","at","be","because","been","but","by","can","cannot","could","dear","did","do","does","either","else","ever","every","for","from","get","got","had","has","have","he","her","hers","him","his","how","however","i","if","in","into","is","it","its","just","least","let","like","likely","may","me","might","most","must","my","neither","no","nor","not","of","off","often","on","only","or","other","our","own","rather","said","say","says","she","should","since","so","some","than","that","the","their","them","then","there","these","they","this","tis","to","too","twas","us","wants","was","we","were","what","when","where","which","while","who","whom","why","will","with","would","yet","you","your"] 

stopWord :: HashSet Text 
stopWord = S.fromList (finnishStop ++ englishStop) 

isStopWord :: Text -> Bool 
isStopWord x = x `S.member` stopWord 

Ihre isStopWord Funktion mit dieser Version Ersetzen führt viel besser und skaliert viel besser (wenn auch definitiv nicht 1-1). Sie könnten auch mit HashMap (aus dem gleichen Paket) anstelle von Map aus den gleichen Gründen betrachten, aber ich habe keine merkliche Veränderung davon.

Eine andere Option ist, die Standard-Heap-Größe zu erhöhen, um einen Teil des Druckes vom GC abzuziehen und ihm mehr Spielraum zu geben. Geben Sie den kompilierten Code eine Standard-Heap-Größe von 1 GB (-H1G Flag), bekomme ich eine GC-Bilanz von etwa 50% auf 4 Kernen, während ich nur ~ 25% ohne (es läuft auch ~ 30% schneller).

Mit diesen beiden Änderungen sinkt die durchschnittliche Laufzeit auf vier Kernen (auf meinem Computer) von ~ 10.5s auf ~ 3.5s. Wohl gibt es Raum für Verbesserungen basierend auf der GC-Statistik (immer noch nur 58% der Zeit produktive Arbeit verbringend), aber tun wesentlich besser könnte eine viel drastischere Änderung an Ihren Algorithmus erfordern.

+3

Ich bin offen für drastische Veränderungen. Das soll ich doch lernen :) – Masse

4

Ich denke, Daniel es richtig - die Data.Map und Listen ein fauler Datenstrukturen sind; Sie sollten sowohl foldl 'als auch insertWith' verwenden, um sicherzustellen, dass die Arbeit für jeden Chunk eifrig erledigt wird --- andernfalls werden alle Arbeiten auf den sequentiellen Teil (reduce) verzögert.

Auch ist es nicht offensichtlich, dass die richtige Granularität für jede Datei einen Funken bildet, besonders wenn die Dateigrößen erheblich abweichen. Wenn das der Fall sein kann, wäre es vorzuziehen, die Wortlisten zu verketten und in Stücke gerader Größe aufzuteilen (siehe parListChunk-Kombinator).

Während Sie dabei sind, würde ich auch einige Fallstricke der Verwendung von Lazy IO (ReadFile) zu öffnen, um viele Dateien zu öffnen (das Laufzeit-System möglicherweise keine Dateizugriffsnummern, weil es zu lange hält).

+0

Wie aus meinem Kommentar zu sehen, habe ich parMap, parListN und parListChunk versucht. Alle haben ähnliche Leistungsmerkmale. Die Umstellung von fold auf faltl 'brachte das Gleichgewicht auf> 2, aber die Gesamtlaufzeit hat sich fast verdoppelt. – Masse

+0

Ich änderte den Code so, dass foldr -> foldl 'und zog die mapreduce von WordList zu Histogramm. Ich spalte die Datei in Zeilen und innerhalb von mapreduce verwende ich parListChunk stratm (100) xs. Ich habe die Laufzeit vervierfacht (von ~ 70 Sekunden auf 300 Sekunden) – Masse