IO над большими файлами в haskell: проблема производительности - PullRequest
6 голосов
/ 24 марта 2011

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

import System 
import qualified Data.ByteString.Lazy as BL 
import Data.Word  
import Data.List

main :: IO () 
main =     
    do         
        args <- System.getArgs         
        let filename = head args         
        byteString <- BL.readFile filename         
        let wordsList = BL.unpack byteString         
        let foldFun acc word = doSomeStuff word : acc
        let wordsListCopy = foldl' foldFun [] wordsList
        let byteStringCopy = BL.pack (reverse wordsListCopy)
        BL.writeFile (filename ++ ".cpy") byteStringCopy
    where
        doSomeStuff = id

Я называю этот файл TestCopy.hs, затем делаю следующее:

$ ls -l *MB
-rwxrwxrwx 1 root root 10000000 2011-03-24 13:11 10MB
-rwxrwxrwx 1 root root  5000000 2011-03-24 13:31 5MB
$ ghc --make -O TestCopy.hs 
[1 of 1] Compiling Main             ( TestCopy.hs, TestCopy.o )
Linking TestCopy ...
$ time ./TestCopy 5MB

real    0m5.631s
user    0m1.972s
sys 0m2.488s
$ diff 5MB 5MB.cpy
$ time ./TestCopy 10MB 

real    3m6.671s
user    0m3.404s
sys 1m21.649s
$ diff 10MB 10MB.cpy 
$ time ./TestCopy 10MB +RTS -K500M -RTS

real    2m50.261s
user    0m3.808s
sys 1m13.849s
$ diff 10MB 10MB.cpy 
$ 

Моя проблема: между 5 МБ и 10 МБ файла есть огромная разница. Я хотел бы, чтобы исполнения были линейными по размеру входного файла. Пожалуйста, что я делаю не так, и как мне этого добиться? Я не против использовать ленивые строки байтов или что-то еще, пока это работает, но это должна быть стандартная библиотека ghc.

Точность: это для университетского проекта. И я не пытаюсь копировать файлы. Функция doSomeStuff должна выполнять действия сжатия / распаковки, которые я должен настроить.

Ответы [ 2 ]

8 голосов
/ 27 марта 2011

Для обработки ввода по частям я бы использовал перечислитель .

import Data.Enumerator
import Data.Enumerator.Binary (enumFile)

Используем строки байтов

import Data.ByteString as BS

и IO

import Control.Monad.Trans (liftIO)
import Control.Monad (mapM_)
import System (getArgs)

Ваша основная функция может выглядеть следующим образом:

main =
  do (filepath:_) <- getArgs
     let destination
     run_ $ enumFile filepath $$ writeFile (filepath ++ ".cpy")

enumFile читает 4096 байт на блок и передает их в writeFile, который записывает их.

enumWrite определяется как:

enumWrite :: FilePath -> Iteratee BS.ByteString IO ()
enumWrite filepath =
  do liftIO (BS.writeFile filepath BS.empty)   -- ensure the destination is empty
     continue step
  where
  step (Chunks xs) =
    do liftIO (mapM_ (BS.appendFile filepath) xs)
       continue step
  step EOF         = yield () EOF

Как вы можете видеть, функция step берет куски строк байтов и добавляет их в файл назначения. Эти чанки имеют тип Stream BS.Bytestring, где Поток определяется как:

data Stream a = Chunks [a] | EOF

На шаге EOF завершается, уступая ().

Для более подробного прочтения, я лично рекомендую Майклу Snoymans учебник

цифры

$ time ./TestCopy 5MB
./TestCopy 5MB  2,91s user 0,32s system 96% cpu 3,356 total

$ time ./TestCopy2 5MB
./TestCopy2 5MB  0,04s user 0,03s system 93% cpu 0,075 total

Это довольно улучшение. Теперь для реализации вашего фолда вы, вероятно, захотите написать Enumeratee, который используется для преобразования входного потока. К счастью, в пакете перечислителя уже определена функция карты, которую можно изменить в соответствии с вашими потребностями, то есть ее можно изменить для переноса состояния.

На построение промежуточного результата

Вы создаете wordsList в обратном порядке и затем обращаетесь к нему. Я думаю, списки различий работают лучше, потому что добавления занимают только O (1) времени из-за того, что добавление является только композицией функций. Я не уверен, занимают ли они больше места все же. Вот приблизительный эскиз списков различий:

type DList a = [a] -> [a]

emptyList :: DList a
emptyList = id

snoc :: DList a -> a -> DList a
snoc dlist a = dlist . (a:)

toList :: DList a -> [a]
toList dlist = dlist []

Этот ответ, вероятно, больше не нужен, но я добавил его для полноты.

2 голосов
/ 24 марта 2011

Я так понимаю, это продолжение Чтение большого файла в haskell? со вчерашнего дня.

Попробуйте скомпилировать с "-rtsopts -O2" вместо простого "-O".

Вы заявляете: «Я хотел бы просматривать входной файл байт за байтом и генерировать выходной байт за байтом».но ваш код читает весь ввод, прежде чем пытаться создать какой-либо вывод.Это просто не очень характерно для цели.

В моей системе я вижу, что "ghc -rtsopts --make -O2 b.hs" дает

(! 741)-> time ./b five
real 0m2.789s user 0m2.235s sys 0m0.518s
(! 742)-> time ./b ten
real 0m5.830s user 0m4.623s sys 0m1.027s

, что сейчас мне кажется линейным.

...