2012-03-17 16 views
9

Supongamos que tengo varios archivos de 200mb + que quiero analizar. ¿Cómo haría esto en Haskell?Análisis de archivos de registro grandes en Haskell

Aquí está mi programa inicial:

import Data.List 
import Control.Monad 
import System.IO 
import System.Environment 

main = do 
    filename <- liftM head getArgs 
    contents <- liftM lines $ readFile filename 
    putStrLn . unlines . filter (isPrefixOf "import") $ contents 

Esto lee todo el archivo en memoria antes de analizar a través de él. Pasé luego con esto:

import Data.List 
import Control.Monad 
import System.IO 
import System.Environment 

main = do 
    filename <- liftM head getArgs 
    file <- (openFile filename ReadMode) 
    contents <- liftM lines $ hGetContents file 
    putStrLn . unlines . filter (isPrefixOf "import") $ contents 

pensé que como hGetContents es perezoso, it will avoid reading the whole file into memory. Pero ejecutar ambos scripts bajo valgrind mostró un uso de memoria similar para ambos. Entonces, mi script está equivocado o valgrind está mal. Recopilo los guiones usando

ghc --make test.hs -prof 

¿Qué me falta? Pregunta de bonificación: veo muchas menciones en SO de cómo Lazy IO en Haskell es en realidad algo malo. ¿Cómo/por qué debería usar IO estricto?

Actualización:

por lo que parece que estaba equivocado en mi lectura de valgrind. Usando +RTS -s, esto es lo que me sale:

7,807,461,968 bytes allocated in the heap 
1,563,351,416 bytes copied during GC 
     101,888 bytes maximum residency (1150 sample(s)) 
     45,576 bytes maximum slop 
      2 MB total memory in use (0 MB lost due to fragmentation) 

Generation 0: 13739 collections,  0 parallel, 2.91s, 2.95s elapsed 
Generation 1: 1150 collections,  0 parallel, 0.18s, 0.18s elapsed 

INIT time 0.00s ( 0.00s elapsed) 
MUT time 2.07s ( 2.28s elapsed) 
GC time 3.09s ( 3.13s elapsed) 
EXIT time 0.00s ( 0.00s elapsed) 
Total time 5.16s ( 5.41s elapsed) 

La línea importante es 101,888 bytes maximum residency, que dice que en un momento dado mi guión estaba usando 101 KB de memoria como máximo. El archivo que estaba revisando era de 44 mb. Así que creo que el veredicto es: readFile y hGetContents son flojos.

Seguimiento pregunta:

¿Por qué veo 7 GB de memoria asignada en el montón? Eso parece muy alto para un script que está leyendo en un archivo de 44 mb.

actualización para el seguimiento pregunta

Parece que unos pocos GB de memoria asignada en el montón no es atípico para Haskell, por lo que no hay motivo de preocupación. Usando ByteString s en lugar de String s toma el uso de la memoria por mucho:

81,617,024 bytes allocated in the heap 
     35,072 bytes copied during GC 
     78,832 bytes maximum residency (1 sample(s)) 
     26,960 bytes maximum slop 
      2 MB total memory in use (0 MB lost due to fragmentation) 
+0

Hum, ¿estás seguro de que no necesitas construir toda la cadena 'unlines' antes de escribirla con' putStrLn'? Me gustaría probar algo como 'Control.Monad.forM_ (filter (isPrefixOf" import)) contents) $ putStrLn'. Sin embargo, es solo una suposición. –

+0

@Riccardo: No, 'unlines' se puede evaluar de forma perezosa. Pruebe 'putStr $ unlines $ map show [1 ..]' en 'ghci'. – ephemient

+0

¿El -O2 resuelve mágicamente el problema? – gspr

Respuesta

5

Tanto readFile y hGetContents debe ser perezoso. Intente ejecutar su programa con +RTS -s y vea cuánta memoria realmente se usa. ¿Qué te hace pensar que todo el archivo se lee en la memoria?

En cuanto a la segunda parte de su pregunta, perezoso IO es a veces en la raíz del inesperado space leaks o resource leaks. No es realmente la culpa de IO perezoso en sí mismo, pero determinar si tiene fugas requiere analizar cómo se usa.

+0

Sí, tienes razón :) ¿Alguna idea sobre mi pregunta de seguimiento? –

+3

@VladtheImpala: No se preocupe por la cifra de asignación total; es la cantidad * total * de memoria asignada durante la vida útil del programa. Nunca disminuye, incluso cuando la memoria se libera mediante la recolección de basura, como ocurre frecuentemente en Haskell; las cifras de múltiples gigabytes por segundo no son infrecuentes. – ehird

+0

@ehird ah bien, gracias. Simplemente no estaba seguro de si eso era típico o no. –

5

Por favor, no use String's lisos (especialmente cuando está procesando archivos> 100m). Sólo reemplazarlos con ByteString 's (o): Data.Text

{-# LANGUAGE OverloadedStrings #-} 

import Control.Monad 
import System.Environment 
import qualified Data.ByteString.Lazy.Char8 as B 

main = do 
    filename <- liftM getArgs 
    contents <- liftM B.lines $ B.readFile filename 
    B.putStrLn . B.unlines . filter (B.isPrefixOf "import") $ contents 

y apuesto, esto va a ser varias veces más rápido.

UPD: con respecto a su pregunta de seguimiento.
La cantidad de memoria asignada está fuertemente conectada a la aceleración mágica cuando se cambia a cadenas de bytes.
Como String es solo una lista genérica, requiere memoria adicional para cada Char: puntero al siguiente elemento, encabezado del objeto, etc. Toda esta memoria debe asignarse y luego volver a recopilarse. Esto requiere mucho poder computacional.
Por otro lado, ByteString es una lista de trozos, es decir, bloques de memoria continuos (creo, no menos de 64 bytes cada uno). Esto reduce en gran medida el número de asignaciones y colecciones, y también mejora la ubicación del caché.

+0

Totalmente de acuerdo con el uso de ByteStrings ... No quería complicar más las cosas al agregar eso a mi ejemplo. Pero sí, son un * enorme * ahorro en términos de tiempo y memoria: '81,617,024 bytes asignados en el montón 'con' 78,832 bytes de residencia máxima 'y' MUT tiempo 0.08s (0.22s transcurridos) 'tiempo. –

Cuestiones relacionadas