2010-10-18 2 views
5

Grüße,Haskell ByteStrings - endend mit großen Datei bis in den Speicher geladen

Ich versuche zu verstehen, warum ich die gesamte Datei in den Speicher mit dem folgenden Programm geladen zu sehen bin, aber wenn Sie die Zeile aus kommentieren unten "(***)" dann läuft das Programm in konstantem (ungefähr 1,5M) Raum.

EDIT: Die Datei ist etwa 660 MB, das Feld in Spalte 26 ist eine Datumszeichenfolge wie '2009-10-01', und es gibt eine Million Zeilen. Der Prozess verwendet etwa 810 MB bis zum Zeitpunkt der 'getLine'

Habe ich recht in der Annahme, es ist mit der Aufspaltung der Zeichenfolge mit 'Split' verbunden, und dass irgendwie die zugrunde liegende ByteString, die aus der Datei gelesen wurde Wird kein Müll gesammelt, weil er immer noch referenziert wird? Aber wenn, dann dachte ich, BS.copy würde das umgehen. Irgendwelche Ideen, wie man die Berechnung erzwingt - ich kann nicht scheinen, Seq 'in den richtigen Platz zu bekommen, um einen Effekt zu haben.

(NB die Quelldatei durch Tabulatoren getrennte Linien)

Vielen Dank im Voraus,

Kevin

module Main where 

import System.IO 
import qualified Data.ByteString.Lazy.Char8 as BS 
import Control.Monad 


type Record = BS.ByteString 

importRecords :: String -> IO [Record] 
importRecords filename = do 
    liftM (map importRecord.BS.lines) (BS.readFile filename) 

importRecord :: BS.ByteString -> Record 
importRecord txt = r 
    where 
    r = getField 26 
    getField f = BS.copy $ ((BS.split '\t' txt) !! f) 

loopInput :: [Record] -> IO() 
loopInput jrs = do 
    putStrLn $ "Done" ++ (show $ last jrs) 
    hFlush stdout 
    x <- getLine 
    return() 

    -- (***) 
    loopInput jrs 

main = do 
    jrs <- importRecords "c:\\downloads\\lcg1m.txt" 
    loopInput jrs 
+1

Es wäre hilfreich, die Datei oder zumindest einige Statistiken darüber zu haben. Ich bin nicht davon überzeugt, dass Ihr Problem nicht darin besteht, alle "[Record]" gleichzeitig im Speicher zu behalten (erzwungen durch den Aufruf "putStrLn ... last jrs"). Die gesamte Liste muss im Speicher bleiben, weil Sie sie in "loopInput" übergeben - wenn Sie nur "last jrs" übergeben haben oder wenn Sie nur den Kopf der Liste verbraucht haben, bevor Sie den Rest erzwingen, können Sie die inkrementelle Verarbeitung im konstanten Speicherbereich durchführen. EDIT: Auch einige Haufen Profilerstellung. –

+0

Die Datei ist ungefähr 660 MB, das Feld in Spalte 26 ist eine Datumszeichenkette wie '2009-10-01', und es gibt eine Million Zeilen. Der Prozess benötigt ungefähr 810 MB, wenn er die 'getLine' erreicht. Prost! – Kevin

Antwort

3

Ihr Anruf zu last die Liste zwingt, jrs. Um das herauszufinden, muss es durch die gesamte Datei laufen, die für jeden Eintrag in jrs Thunks aufbaut. Da Sie nicht jedes Element in jrs (außer dem letzten) bewerten, hängen diese Thunks mit Verweisen auf den Byte-String, so dass diese im Speicher bleiben müssen.

Die Lösung besteht darin, die Auswertung dieser Thunks zu erzwingen. Weil wir über Raum reden das erste, was ich tat, war eigentlich Ihre Daten in einem kleineren Format zu speichern:

type Year = Word16 
type Month = Word8 
type Day = Word8 
data Record = Rec {-# UNPACK #-} !Year {-# UNPACK #-} !Month {-# UNPACK #-} !Day 
     deriving (Eq, Ord, Show, Read) 

Dies reduziert das hässlich 10 Byte Bytestring (+ Overhead von ~ 16 Bytes von Strukturinformationen) auf rund 8 Bytes.

importRecord hat jetzt toRecord r rufen die richtige Art zu erhalten:

toRecord :: BS.ByteString -> Record 
toRecord bs = 
    case BS.splitWith (== '-') bs of 
      (y:m:d:[]) -> Rec (rup y) (rup m) (rup d) 
      _ -> Rec 0 0 0 

rup :: (Read a) => BS.ByteString -> a 
rup = read . BS.unpack 

Wir brauchen Daten evalute wenn wir ByteString-Record konvertieren, können so verwenden Sie die parallel Paket und definieren eine NFData Instanz von .

instance NFData Record where 
    rnf (Rec y m d) = y `seq` m `seq` d `seq`() 

Jetzt sind wir bereit zu gehen, ich modifizierte Haupt evalList zu verwenden, wodurch die gesamte Liste zu zwingen, bevor Sie Ihre Funktion, die die letzte will:

main = do 
    jrs <- importRecords "./tabLines" 
    let jrs' = using jrs (evalList rdeepseq) 
    loopInput jrs' 

Und wir können den Haufen Profil sehen sieht gut aus (und stimmt überein, verwendet das Programm sehr wenig Speicher).

alt text

Tut mir leid, dass andere falsche Antwort irreführend - ich war auf der Tatsache, dass süchtig inkrementelle Verarbeitung es fixiert und wußte nicht wirklich die Thunks wirklich um hing, nicht sicher, warum glitt mein Gehirn über Das. Obwohl ich dem Kern zutraue, sollten Sie diese Informationen inkrementell verarbeiten, sodass all diese Antworten strittig sind.

Bitte beachten Sie, dass der riesige Byte-String nicht in den vorherigen Heap-Profilen angezeigt wurde, die ich gepostet habe, da Fremdzuweisungen (einschließlich ByteString) nicht vom Heap-Profiler verfolgt werden.

+0

Danke für die umfassende Antwort; Es ist aufschlussreich, die Verwendung von seq/deepSeq zu sehen, um die Auswertung zu erzwingen, aber ich sehe immer noch nicht, warum dieses Programm überhaupt nicht funktioniert. Es hängt davon ab, ob die Linie unter (\ * \ * \ *) da ist; Wenn dies der Fall ist, bleibt der große Bytestring im Speicher hängen. Wenn ich die Zeile "show (last jrs)" editiere um "show jrs" zu lesen, dann sollte dies auch dazu führen, dass die * gesamte * Liste ausgewertet wird -korrekt? In diesem Fall läuft das Programm erneut in 1-2M ohne die Zeile (\ * \ * \ *), hält aber die Datei im Speicher, wenn "loopInput jrs" erneut aufgerufen wird. Tut mir leid, wenn ich ein bisschen dick bin! – Kevin

+0

Mit der oben genannten "Record" Definition (~ 24 Bytes pro Eintrag, 4.1M Einträge) und 'show jrs' anstelle von' show $ last jrs' sehe ich 300MB (~ 100MB für die Aufnahmeliste, 50MB von?, 2x zum Kopieren von GC). Ich denke, das fremde Alloc wird nur Blöcke von 64+ Bytes zuweisen, also haben Sie für jeden Ihrer 1M Einträge 1 Wort für den LPS Konstruktor, 1 Wort für den PS Konstruktor, 1 Wort für den Zeiger, 2 Wörter für die Länge und den Offset , 1 Wort für das nächste LPS Feld, 64+ Bytes für die tatsächlichen Daten, Listen nehmen 3 Wörter -> 9 * 8 + 64 = 136B, 136MB für 1M Einträge * 2 für GC + Potential irgendwelche Unbekannten. –

+0

Kevin: Die Mathematik in meinem vorherigen Kommentar ergibt nicht Ihre 800MB Verwendung (ich weiß, ich sehe das), aber seinen Fortschritt. Vielleicht könntest du ein Profiling durchführen oder deinen BS.ByteString durch einen gepackten Datensatz (wie meinen) ersetzen und sehen, wie es läuft? –

1

Es scheint zwei Fragen hier zu sein:

  • warum die Speicherauslastung abhängig von der Anwesenheit oder Abwesenheit der Linie (***);
  • Warum ist die Speicherauslastung mit (***) etwa 800 MB statt etwa 40 MB.

Ich weiß nicht wirklich, was ich über die erste sagen soll, die TomMD nicht schon gesagt hat; Innerhalb der loopInput-Schleife kann jrs nie freigegeben werden, da es als Argument für den rekursiven Aufruf von loopInput benötigt wird. (Sie wissen, dass return() nichts tut, wenn (***) vorhanden ist, nicht wahr?)

Wie für die zweite Frage, ich denke, Sie haben Recht, dass die Eingabe ByteString nicht Müll gesammelt wird. Der Grund ist, dass Sie nie die Elemente Ihrer Liste jrs neben der letzten auswerten, so dass sie immer noch Verweise auf den ursprünglichen ByteString enthalten (obwohl sie das Format BS.copy ... haben). Ich würde denken, dass das Ersetzen von show $ last jrs durch show jrs Ihre Gedächtnisnutzung verringern würde; macht es? Alternativ können Sie eine strengere Karte versuchen, wie

map' f []  = [] 
map' f (x:xs) = ((:) $! (f $! x)) (map' f xs) 

Ersetzen Sie die map in importRecords mit map' und sehen, ob die Speicherauslastung reduziert.

+0

Ich habe versucht, die "Show jrs" -Modifikation, aber es hat nicht die Erinnerung leider reduziert. Vielen Dank für Ihre Hilfe, die hier veröffentlichten Antworten waren für mich aufschlussreich. – Kevin

Verwandte Themen