[Haskell] readFile: in termini di performance e spazio cos'è meglio?
Ciao 
Uso [tt]readFile[/tt] abbastanza spensieratamente (tipicamente ne spezzo il risultato in linee, filtro, mappo, blb bla...). Tuttavia mi chiedo se abuso, sto fraintendendo la lazyness di Haskell. Un esempio pratico semplice semplice per spiegarmi un po' meglio.
Problema. Si ha un file "num.txt" che contiene un numero per riga (rigorosamente): definire una funzione che dica la somma dei dati lì dentro.
In termini di velocità e spazio la seconda implementazione mi sembra migliore, però Haskell è lazy e quindi potrebbe non cambiare niente...

Uso [tt]readFile[/tt] abbastanza spensieratamente (tipicamente ne spezzo il risultato in linee, filtro, mappo, blb bla...). Tuttavia mi chiedo se abuso, sto fraintendendo la lazyness di Haskell. Un esempio pratico semplice semplice per spiegarmi un po' meglio.
Problema. Si ha un file "num.txt" che contiene un numero per riga (rigorosamente): definire una funzione che dica la somma dei dati lì dentro.
-- sandbox.hs import Control.Conditional (ifM) import System.IO (Handle, IOMode(..), hGetLine, hIsEOF, withFile) -- one line version sumFile1 :: FilePath -> IO Double sumFile1 f = fmap (sum . map read . lines) (readFile f) -- and a Lisp-like approach sumFile2 :: FilePath -> IO Double sumFile2 f = withFile f ReadMode (_sum 0) where _sum :: Double -> Handle -> IO Double _sum s hdl = ifM (hIsEOF hdl) (return s) $ do x <- fmap read (hGetLine hdl) _sum (s+x) hdl
In termini di velocità e spazio la seconda implementazione mi sembra migliore, però Haskell è lazy e quindi potrebbe non cambiare niente...

Risposte
Beh, puoi controllare chi è piu veloce, no? C'è un comando [inline]time[/inline] in ghc, o qualcosa del genere...
Ci ho già provato per quel mi concerne: ho generato un file con tipo \(3000\) righe di numeri a caso
E ho provato entrambe le funzioni con [tt]time[/tt]: non sono un esperto, ma le voci "real", "user" e "sys" mi sembravano comparabili, nel senso che su più test una volta la prima era leggermente più rapida dell'altra, un volta la seconda più della prima. Solo che: \(3000\) righe sono poche? poi come faccio ad ottenere un analisi di spazio "consumato"?
~/sandbox$ for i in {1..3000}; do echo $RANDOM >> num.ls; done
E ho provato entrambe le funzioni con [tt]time[/tt]: non sono un esperto, ma le voci "real", "user" e "sys" mi sembravano comparabili, nel senso che su più test una volta la prima era leggermente più rapida dell'altra, un volta la seconda più della prima. Solo che: \(3000\) righe sono poche? poi come faccio ad ottenere un analisi di spazio "consumato"?
Il mio consiglio è quello di usare le stesse funzioni, ma rese disponibili dal modulo [tt]Data.ByteString.Lazy.Char8[/tt] (devi importarlo [tt]qualified[/tt]). Dovrebbe essere più efficiente rispetto all'uso di stringhe come nel tuo caso.
Credo che nel tuo caso le due funzioni siano più o meno equivalenti. Comunque puoi leggere come fare il profiling di un codice haskell per esempio qui.
Credo che nel tuo caso le due funzioni siano più o meno equivalenti. Comunque puoi leggere come fare il profiling di un codice haskell per esempio qui.
Inoltre dovresti aumentare tanto il numero di righe… \(3000\) numeri sono infatti molto pochi. Se vuoi vedere differenze inizia a passare a qualche milione.
Ciao.
Allora sono andato a informarmi un po' sul profiling. Ho fatto dei tentativi su file [tt]num.ls[/tt] di \(3 \cdot 10^6\) righe, che possono essere poche, ma bastano a trarre delle conclusioni.
1. Tra [tt]sumFile1[/tt] e [tt]sumFile2[/tt] il confronto è impari: andando a vedere come sono implementate le funzioni di Prelude, [tt]sum[/tt] è strict, mentre nella seconda ci sono dei pezzi pigri.
2. La seconda implementazione è più lenta rispetto alla prima, ma di poco.
3. Sulla seconda versione ho più controllo sul processo di valutazione, e quindi sulla memoria.
Proviamo così allora:
Ho azzerato lo spazio consumato, ridotto ancora di più i tempi rispetto a [tt]sumFile1[/tt] e la "productivity" si avvicina al 100% (mentre con [tt]sumFile1[/tt] si sta al 91%). È tutta questione di strictness allora.
Per quanto riguarda ByteString: so che esiste e non è difficile passarci, ma la cosa era un esercizietto.
Allora sono andato a informarmi un po' sul profiling. Ho fatto dei tentativi su file [tt]num.ls[/tt] di \(3 \cdot 10^6\) righe, che possono essere poche, ma bastano a trarre delle conclusioni.
1. Tra [tt]sumFile1[/tt] e [tt]sumFile2[/tt] il confronto è impari: andando a vedere come sono implementate le funzioni di Prelude, [tt]sum[/tt] è strict, mentre nella seconda ci sono dei pezzi pigri.
2. La seconda implementazione è più lenta rispetto alla prima, ma di poco.
3. Sulla seconda versione ho più controllo sul processo di valutazione, e quindi sulla memoria.
Proviamo così allora:
-- sandbox.hs {-# LANGUAGE BangPatterns #-} import Control.Conditional (ifM) import System.IO (Handle, hGetLine, hIsEOF, IOMode(..), withFile) main :: IO () main = print =<< sumFile "num.ls" sumFile :: FilePath -> IO Double sumFile f = withFile f ReadMode (_sum 0) where _sum :: Double -> Handle -> IO Double _sum !s hdl = ifM (hIsEOF hdl) (return s) $ do x <- (fmap read . hGetLine) hdl _sum (s+x) hdl
$ ghc -O -rtsopts sandbox.hs $ for i in {1..3000000}; do echo $RANDOM >> num.ls; done $ ./sandbox +RTS -sstderr 4.9164487364e10 16,747,949,904 bytes allocated in the heap 11,267,488 bytes copied during GC 53,592 bytes maximum residency (3 sample(s)) 36,520 bytes maximum slop 0 MB total memory in use (0 MB lost due to fragmentation) Tot time (elapsed) Avg pause Max pause Gen 0 16060 colls, 0 par 0.087s 0.086s 0.0000s 0.0001s Gen 1 3 colls, 0 par 0.001s 0.001s 0.0002s 0.0004s INIT time 0.000s ( 0.000s elapsed) MUT time 5.378s ( 5.411s elapsed) GC time 0.088s ( 0.086s elapsed) EXIT time 0.000s ( 0.000s elapsed) Total time 5.466s ( 5.497s elapsed) %GC time 0.0% (0.0% elapsed) Alloc rate 3,114,094,493 bytes per MUT second Productivity 98.4% of total user, 98.4% of total elapsed
Ho azzerato lo spazio consumato, ridotto ancora di più i tempi rispetto a [tt]sumFile1[/tt] e la "productivity" si avvicina al 100% (mentre con [tt]sumFile1[/tt] si sta al 91%). È tutta questione di strictness allora.
Per quanto riguarda ByteString: so che esiste e non è difficile passarci, ma la cosa era un esercizietto.