Haskell ByteStrings - заканчивается большим файлом, загруженным в память - PullRequest
5 голосов
/ 18 октября 2010

Привет,

Я пытаюсь понять, почему я вижу весь файл, загруженный в память с помощью следующей программы, но если вы закомментируете строку ниже "(***)", тогдаПрограмма работает в постоянном (около 1,5 м) пространстве.

РЕДАКТИРОВАТЬ: размер файла составляет около 660 МБ, поле в столбце 26 представляет собой строку даты, например «2009-10-01», и содержит один миллион строк.Процесс использует около 810 МБ к моменту попадания в 'getLine'

Правильно ли я считаю, что это связано с разбиением строки с использованием 'split', и что каким-то образом базовая строка ByteString была прочитана изфайл не может быть собран мусором, потому что на него все еще ссылаются?Но если так, то я думал, что BS.copy будет работать вокруг этого.Любые идеи, как форсировать вычисления - я не могу заставить 'seq' попасть в нужное место, чтобы иметь эффект.

(Примечание: исходный файл - строки, разделенные табуляцией)

Заранее спасибо,

Кевин

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

Ответы [ 2 ]

3 голосов
/ 19 октября 2010

Ваш вызов last форсирует список, jrs. Чтобы понять это, он должен пройти через все файлы для создания записи для каждой записи в jrs. Поскольку вы не оцениваете каждый элемент в jrs (кроме последнего), эти блоки выводятся со ссылками на строку байтов, поэтому они должны оставаться в памяти.

Решение состоит в том, чтобы форсировать оценку этих громов. Поскольку мы говорим о космосе, первое, что я сделал, было на самом деле сохранить вашу информацию в меньшем формате:

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

Это уменьшает эту уродливую 10-байтовую строку байтов (+ издержки ~ 16 байтов информации о структуре) примерно до 8 байтов.

importRecord теперь нужно позвонить toRecord r, чтобы получить правильный тип:

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

Нам потребуется выполнить оценку данных при преобразовании из ByteString в Record, поэтому давайте используем пакет параллельный и определяем экземпляр NFData из DeepSeq .

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

Теперь мы готовы к работе, я изменил main для использования evalList, таким образом форсируя весь список перед вашей функцией, которая хочет последнюю:

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

И мы можем видеть, что профиль кучи выглядит красиво (и top согласен, программа использует очень мало памяти).

alt text

Извините за этот другой вводящий в заблуждение неправильный ответ - я подсел на тот факт, что пошаговая обработка исправляет это, и не осознавал, что на самом деле торчали громоотводы. Несмотря на то, что я поддерживаю эту суть, вы должны постепенно обрабатывать эту информацию, делая весь этот ответ спорным.

К вашему сведению, огромная строка байтов не отображалась в тех предыдущих профилях кучи, которые я опубликовал, поскольку внешние выделения (включая ByteString) не отслеживаются профилировщиком кучи.

1 голос
/ 20 октября 2010

Здесь, кажется, есть два вопроса:

  • почему использование памяти зависит от наличия или отсутствия строки (***);
  • почему использование памяти с (***) составляет около 800 МБ, а не, скажем, 40 МБ.

Я действительно не знаю, что сказать о первой, которую TomMD еще не сказал; внутри цикла loopInput, jrs никогда не может быть освобожден, потому что это необходимо в качестве аргумента для рекурсивного вызова loopInput. (Вы знаете, что return () ничего не делает, когда присутствует (***), верно?)

Что касается второго вопроса, я думаю, что вы правы, что входная строка ByteString не является сборщиком мусора. Причина в том, что вы никогда не оцениваете элементы вашего списка jrs, кроме последнего, поэтому они все еще содержат ссылки на исходную строку ByteString (даже если они имеют форму BS.copy ...). Я думаю, что замена show $ last jrs на show jrs уменьшит использование вашей памяти; Является ли? В качестве альтернативы, вы можете попробовать более строгую карту, например

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

Замените map в importRecords на map' и посмотрите, уменьшает ли это использование памяти.

...