Разбор больших файлов журнала в Haskell

Предположим, у меня есть несколько файлов размером 200 Мбайт, которые я хочу пропустить. Как я могу сделать это в Haskell?

Здесь моя первоначальная программа:

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

Это считывает весь файл в памяти перед его синтаксическим разбором. Затем я пошел с этим:

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

Я подумал, что, поскольку hGetContents ленив, он не сможет прочитать весь файл в памяти. Но запуск обоих сценариев под valgrind показал одинаковое использование памяти для обоих. Поэтому либо мой script ошибочен, либо valgrind неверен. Я скомпилирую скрипты с помощью

ghc --make test.hs -prof

Что мне не хватает? Бонусный вопрос: я вижу много упоминаний о том, как Lazy IO в Haskell на самом деле плохо. Как/почему я должен использовать строгий IO?

Update:

Итак, похоже, что я ошибся в своем чтении valgrind. Используя +RTS -s, вот что я получаю:

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)

Важная строка 101,888 bytes maximum residency, в которой говорится, что в любой заданной точке мой script использовал максимум 101 кб памяти. Файл, который я просматривал, составлял 44 мб. Поэтому я считаю, что вердикт: readFile и hGetContents являются ленивыми.

Последующий вопрос:

Почему я вижу 7gb памяти, выделенной в куче? Это кажется действительно высоким для script, который читается в файле размером 44 МБ.

Обновление для последующего вопроса

Похоже, что несколько Гб памяти, выделенной в куче, не являются нетипичными для Haskell, поэтому нет причин для беспокойства. Использование ByteString вместо String сильно сокращает использование памяти:

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)
2 ответа

Оба readFile и hGetContents должны быть ленивыми. Попробуйте запустить программу с помощью +RTS -s и посмотрите, сколько памяти фактически используется. Что заставляет вас думать, что весь файл читается в памяти?

Что касается второй части вашего вопроса, ленивый IO иногда находится в корне неожиданного утечки пространства или утечки ресурсов. На самом деле это не ошибка ленивого ИО и сама по себе, но определение того, требует ли его утечка анализа того, как он используется.


Пожалуйста, не используйте обычный String (особенно при обработке > 100 м файлов). Просто замените их на ByteString (или 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

И я уверен, это будет в несколько раз быстрее.

UPD: относительно вашего последующего вопроса. Количество выделенной памяти сильно связано с магическим ускорением при переключении на байты. Поскольку String является всего лишь общим списком, для каждого элемента Char: требуется дополнительная память: указатель на следующий элемент, заголовок объекта и т.д. Вся эта память должна быть выделена, а затем собрана обратно. Это требует большой вычислительной мощности. С другой стороны, ByteString - это список кусков, т.е. Непрерывных блоков памяти (я думаю, не менее 64 байт каждый). Это значительно уменьшает количество распределений и коллекций и улучшает локальность кэша.

licensed under cc by-sa 3.0 with attribution.