2010-10-18 9 views
5

Saluti,Haskell stringhe di byte - per finire con file di grandi dimensioni caricati in memoria

Sto cercando di capire perché sto vedendo l'intero file caricato in memoria con il seguente programma, ma se si commento la riga sotto "(***)" quindi il programma viene eseguito in uno spazio costante (circa 1,5 M).

MODIFICA: il file è di circa 660 MB, il campo nella colonna 26 è una stringa di data come '2009-10-01' e ci sono un milione di righe. Il processo utilizza circa 810 MB nel momento in cui raggiunge il 'getLine'

Ho ragione nel ritenere che sia correlato alla divisione della stringa utilizzando 'split' e che in qualche modo il sottostante ByteString che è stato letto dal file possa essere raccolto dai rifiuti perché è ancora referenziato? Ma se è così, allora pensavo che BS.copy avrebbe funzionato su questo. Qualche idea su come forzare il calcolo - Non riesco a ottenere 'seq' nel posto giusto per avere un effetto.

(NB il file sorgente è linee separati da tabulazione)

Grazie in anticipo,

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

Sarebbe utile avere il file o almeno alcune statistiche a riguardo. Non sono convinto che il tuo problema non sia il risultato di mantenere tutti i '[Record]' in memoria in una volta (forzato dalla chiamata 'putStrLn ... last jrs'). L'intero elenco deve rimanere in memoria perché lo passi in 'loopInput' - se hai passato solo' last jrs', o se hai consumato solo il capo dell'elenco prima di forzare il resto, allora potresti eseguire un'elaborazione incrementale dello spazio costante. EDIT: Inoltre, fare un po 'di profilazione dell'heap. –

+0

Il file è di circa 660 MB, il campo nella colonna 26 è una stringa di data come '2009-10-01' e ci sono un milione di righe. Il processo utilizza circa 810 MB nel momento in cui raggiunge il "getLine". Saluti! – Kevin

risposta

3

La chiamata al last costringe la lista, jrs. Per capirlo, è necessario eseguire l'intero file creando i thunk per ogni voce in jrs. Poiché non si sta valutando ogni elemento in jrs (eccetto l'ultimo), questi thunk si bloccano con riferimenti al bytestring, quindi questo deve rimanere in memoria.

La soluzione è forzare la valutazione di quei thunk. Perché stiamo parlando di spazio la prima cosa che ho fatto è stato effettivamente memorizzare le informazioni in un formato più piccolo:

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

Questo riduce quella brutta 10 byte Bytestring (+ sovraccarico di ~ 16 byte di informazioni di struttura) a circa 8 byte.

importRecord deve ora chiamare toRecord r per ottenere il giusto tipo:

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 

Dovremo evalute dati quando convertiamo ByteString-Record, così lascia utilizzare il pacchetto parallel e definire un'istanza NFData da DeepSeq.

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

Ora siamo pronti ad andare, ho modificato principale di usare evalList, costringendo così l'intera lista prima della funzione che vuole l'ultima:

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

e possiamo vedere il profilo mucchio sembra bello (e top è d'accordo, il programma usa pochissima memoria).

alt text

Mi dispiace che altri fuorviante risposta sbagliata - mi è stato agganciato sul fatto che l'elaborazione incrementale la fissa e non ha veramente realizzare le thunk davvero erano appesi in giro, non so perché il mio cervello scivolava sopra quella. Sebbene io rispetti l'essenza, dovresti elaborare in modo incrementale queste informazioni rendendo tutta questa risposta discutibile.

FYI l'enorme test non è stato visualizzato in quei profili di heap precedenti che ho inviato perché le allocazioni esterne (che includono ByteString) non sono tracciate dal profiler dell'heap.

+0

Grazie per la risposta completa; è perspicace vedere l'uso di seq/deepSeq per forzare la valutazione, ma non vedo ancora perché questo programma si comporti in modo anomalo. Sembra dipendere dal fatto che la linea sottostante (\ * \ * \ *) sia presente; se è così, il grande test si blocca in memoria. Se modifico la riga "show (last jrs)" per leggere "show jrs" allora anche questo dovrebbe forzare la * intera * lista da valutare -corretta? In questo caso, il programma viene eseguito nuovamente in 1-2 M senza la riga (\ * \ * \ *), ma conserva il file in memoria se "loopInput jrs" viene richiamato di nuovo. Scusa se sono un po 'spessa! – Kevin

+0

Utilizzando la precedente definizione 'Record' (~ 24 byte per voce, 4,1M voci) e' show jrs' invece di 'show $ last jrs' vedo 300 MB presi (~ 100 MB per la lista Record, 50 MB di?, 2x per copiare GC). Penso che l'allocazione esterna allocherà solo blocchi di 64+ byte, quindi per ciascuna delle tue voci 1M hai 1 parola per il costruttore LPS, 1 parola per il costruttore PS, 1 parola per il puntatore, 2 parole per la lunghezza e l'offset , 1 word per il campo Next LPS, 64+ byte per i dati effettivi, le liste prendono 3 parole -> 9 * 8 + 64 = 136B, 136 MB per le voci 1M * 2 per GC + potenziali eventuali incognite. –

+0

Kevin: La matematica nel mio commento precedente non si aggiunge al tuo uso di 800 MB (lo so, lo vedo), ma il suo progresso. Forse potresti eseguire una profilazione o sostituire BS.ByteString con un record compresso (come il mio) e vedere come vanno le cose? –

1

Sembra che ci siano due domande qui:

  • perché l'utilizzo della memoria dipende dalla presenza o assenza della linea (***);
  • perché l'utilizzo della memoria con (***) è presente a circa 800 MB, anziché, ad esempio, 40 MB.

Non so davvero cosa dire del primo che TomMD non ha già detto; all'interno del ciclo loopInput, non è mai possibile liberare jrs perché è necessario come argomento per la chiamata ricorsiva di loopInput. (Lo sai che return() non fa nulla quando (***) è presente, vero?)

Per quanto riguarda la seconda domanda, penso che tu abbia ragione sul fatto che l'input ByteString non viene raccolto. Il motivo è che non si valutano mai gli elementi del proprio elenco jrs oltre a quello precedente, quindi contengono ancora riferimenti al ByteString originale (anche se sono del formato BS.copy ...). Penserei che la sostituzione di show $ last jrs con show jrs ridurrebbe l'utilizzo della memoria; lo fa? In alternativa, si potrebbe provare una mappa più severo, come

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

Sostituire il map in importRecords con map' e vedere se che riduce l'utilizzo della memoria.

+0

Ho provato la modifica "show jrs", ma non ha ridotto la memoria tristemente. Grazie per il tuo aiuto, però, le risposte pubblicate qui sono state perspicaci per me. – Kevin

Problemi correlati