在Haskell中解析大型日志文件

Vla*_*ala 10 haskell

假设我有几个200mb +文件,我想要通过.我怎么在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
Run Code Online (Sandbox Code Playgroud)

这会在解析之前将整个文件读入内存.然后我去了:

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
Run Code Online (Sandbox Code Playgroud)

我以为既然hGetContents很懒,就会避免将整个文件读入内存.但是,运行这两个脚本valgrind显示两者的内存使用量相似.所以要么我的脚本错了,要么valgrind错了.我使用编译脚本

ghc --make test.hs -prof
Run Code Online (Sandbox Code Playgroud)

我错过了什么?奖金问题:我看到很多关于Haskell中的懒惰IO实际上是一件坏事的提及.我/为什么要使用严格的IO?

更新:

所以在我阅读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)
Run Code Online (Sandbox Code Playgroud)

重要的是101,888 bytes maximum residency,在任何给定的点上我的脚本最多使用101 kb的内存.我翻阅的文件是44 MB.所以我认为判决结果是:readFile并且hGetContents都是懒惰的.

后续问题:

为什么我在堆上看到7gb的内存分配?对于一个读取44 MB文件的脚本来说,这似乎非常高.

更新后续问题

看起来堆上分配的几gb内存对于Haskell来说并不典型,所以不必担心.使用ByteStrings而不是Strings会占用大量内存:

  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)
Run Code Online (Sandbox Code Playgroud)

max*_*kin 8

请不要使用普通String的(特别是在处理> 100m文件时).只需用ByteString's(或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
Run Code Online (Sandbox Code Playgroud)

我打赌,这会快几倍.

UPD:关于你的后续问题.
切换到字节串时,分配的内存量与魔术加速密切相关.
由于String它只是一个通用列表,它需要额外的内存Char:指向下一个元素,对象头等的指针.所有这些内存需要分配然后收回.这需要大量的计算能力.
另一方面,ByteString是一个列表,即连续的内存块(我认为,每个不少于64个字节).这大大减少了分配和集合的数量,并且还改善了缓存局部性.


eph*_*ent 6

双方readFilehGetContents应懒惰.尝试运行程序,+RTS -s看看实际使用了多少内存.是什么让你认为整个文件被读入内存?

至于问题的第二部分,懒惰IO有时是意外空间泄漏资源泄漏的根源.并不是懒惰IO本身的错误,但确定其泄漏是否需要分析它是如何使用的.

  • @VladtheImpala:不要担心总分配数字; 它是在程序生命周期内分配的*总*内存量.它永远不会减少,即使内存被垃圾收集释放,就像在Haskell中经常发生的那样; 每秒几千兆字节的数字并不罕见. (3认同)