在Haskell中解析大型日志文件

10

假设我有几个200mb+的文件要通过grep命令搜索其中的文本。在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运行两个脚本时,两者的内存使用情况相似。所以我的脚本可能有误,或者valgrind有误。我使用以下命令编译脚本:

ghc --make test.hs -prof

我错过了什么?额外问题:我在 Stack Overflow 上看到很多提到 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)
重要的一行是101,888 bytes maximum residency,它表明在任何时候我的脚本最多使用了101 kb的内存。我要搜索的文件大小为44 mb。因此,我的结论是:readFilehGetContents 都是惰性的。
后续问题:
为什么我看到堆上分配了7GB的内存?对于一个读取44MB文件的脚本来说,这似乎非常高。
后续问题的更新:
看起来在Haskell中分配几个GB的堆内存并不是不寻常的,所以没有什么可担心的。使用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)

嗯,你确定在使用 putStrLn 输出之前不需要构建整个 “unlines” 字符串吗?我会尝试使用像 Control.Monad.forM_ (filter (isPrefixOf "import") contents) $ putStrLn 这样的代码。但这只是我的猜测。 - Riccardo T.
@Riccardo:不,unlines可以进行惰性求值。在ghci中尝试运行putStr $ unlines $ map show [1..] - ephemient
-O2 能神奇地解决这个问题吗? - gspr
@gspr:不是的。同样的事情:在堆中分配了7,807,461,320字节 - Vlad the Impala
2个回答

8

请不要使用 String(特别是在处理大于 100 Mb 的文件时)。请使用 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

我敢打赌,这会快几倍。

更新:关于你的后续问题。
当切换到字节字符串时,分配的内存量与神奇的加速有很强的联系。
因为String只是一个通用列表,每个Char都需要额外的内存:指向下一个元素、对象头等。所有这些内存都需要被分配,然后再回收。这需要大量的计算能力。
另一方面,ByteString是一系列的列表,即连续的内存块(我想至少每个64字节)。这极大地减少了分配和回收的数量,并提高了缓存本地性。


绝对同意使用ByteStrings...我不想通过在我的示例中添加它来使事情更加复杂。但是,它们在时间和内存方面都是巨大的节省81,617,024字节分配在堆中,最大驻留量为78,832字节MUT时间0.08秒(0.22秒经过) - Vlad the Impala

6

无论是readFile还是hGetContents都应该是惰性的。尝试使用+RTS -s运行程序,并查看实际使用了多少内存。什么让你认为整个文件都被读入了内存?

至于你问题的第二部分,惰性IO有时会导致意外的空间泄漏资源泄漏。这并不是惰性IO本身的错,但确定它是否存在泄漏需要分析它的使用方式。


3
不用担心总分配数;它是程序生命周期内分配的内存量。即使在Haskell中进行垃圾回收并释放内存时(这经常发生),总内存量也不会减少;每秒多达几千兆字节的数字并不罕见。 - ehird
@ehird 哦,好的,谢谢。我只是不确定那是否是典型的情况。 - Vlad the Impala

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接