简单的解析器耗尽内存

时间:2016-12-29 11:20:26

标签: haskell memory-leaks attoparsec

我想了解为什么这个简单的解析器会耗尽大文件的内存。我真的很无知我做错了什么。

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

parseLine :: Parser String
parseLine = manyTill' anyChar (endOfLine <|> endOfInput)

parseAll :: Parser [Int]
parseAll = manyTill' 
        (parseLine >> (return 0)) -- discarding what's been read
        endOfInput

main :: IO()
main = do 
        [fn] <- getArgs
        text <- B.readFile fn

        case Lazy.parse parseAll text of
                Lazy.Fail _ _ _ -> putStrLn "bad"
                Lazy.Done _ _ -> putStrLn "ok" 

我用:

运行程序
 runhaskell.exe test.hs x.log

输出:

test.hs: Out of memory

x.log大小约为500MB。我的机器有16GB的RAM。

2 个答案:

答案 0 :(得分:4)

如果你看the documentation of attoparsec,你会注意到有类似的例子,并附有以下评论:

  

请注意重叠的解析器anyCharstring "-->"。虽然这会起作用,但效率不高,因为它会导致大量的回溯。

使用拒绝anyChar接受的字符的endOfLine替代方法应解决问题。 E.g。

satisfy (\c -> c `notElem` ['\n', '\r'])

答案 1 :(得分:3)

我对Attoparsec并不熟悉,但我认为你可能很难单独使用它来解析常量内存中的大文件。如果您将顶级解析器parseAll替换为:

parseAll :: Parser ()
parseAll = skipMany anyChar

并对其进行分析,您会发现内存使用仍然无限制地增长。 (当我将您的代码转换为使用严格ByteString s的增量读取时,它没有任何区别。)

我认为问题是这样的:因为Attoparsec会进行自动回溯,所以必须为parseAll(您的版本或我的版本 - 无关紧要)做好准备,以便像这样使用:

(parseAll <* somethingThatDoesntMatch) <|> parseDifferently

如果parseAll解析了50万行并到达结尾,somethingThatDoesntMatch将导致它一直回溯到开头,然后用parseDifferently重新解析所有内容。因此,在解析完成之前,无法释放回溯的元信息和ByteStrings本身。

现在,你的解析器(以及我上面的例子),“显然”不需要以这种方式回溯,但是Attoparsec并没有推断出这一点。

我可以想到几种方法:

  1. 如果您正在解析兆字节而不是千兆字节,请考虑使用Parsec,它只有在明确显示时才会回溯(例如,使用try)。
  2. 使用手工制作的非回溯解析器将日志文件拆分为行(或行块),并在每行/块上运行Attoparsec解析器。