【问题标题】:IO over big files in haskell: Performance issue对haskell中的大文件进行IO:性能问题
【发布时间】:2011-03-24 13:08:14
【问题描述】:

我正在尝试使用 Haskell 处理大文件。我想逐字节浏览输入文件,并逐字节生成输出。当然,我需要用合理大小(几 KB)的块来缓冲 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 
$ 

我的问题:5MB 和 10MB 文件之间存在巨大差异。我希望性能与输入文件的大小成线性关系。请问我做错了什么,我该如何做到这一点?我不介意使用惰性字节串或其他任何东西,只要它可以工作,但它必须是标准的 ghc 库。

Precision:适用于大学项目。而且我不是要复制文件。 doSomeStuff 函数将执行我必须自定义的压缩/解压缩操作。

【问题讨论】:

  • ByteString 的packunpack 是非常昂贵的操作。你不能doSomeStuff 直接用ByteString 吗?注意:惰性 ByteString 在内部被“缓冲”,这可能足以满足您的任务
  • 我只是在没有打包和解包的情况下尝试过,直接处理字节串,结果更长,但我仍然在 5MB 和 10MB 之间有这么大的差异。
  • 也许您可以在某处发布一个完整的代码示例来演示问题?
  • @Ed'ka 但这就是我所做的。上面 TestCopy.hs 的工作代码说明了这个问题,因为 10MB 的文件在 3 分钟内被复制。
  • cabal unpack blaze-builder -- 现在您有了源代码,可以将您需要的任何文件直接移动到您的存储库中。

标签: io lazy-evaluation bytestring haskell


【解决方案1】:

对于分块输入处理,我会使用 enumerator 包。

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,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 步骤终止时,产生 ()。

要对此进行更详细的阅读,我个人推荐迈克尔 斯诺曼斯tutorial

数字

$ 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,它用于转换输入流。幸运的是,在 enumerator 包中已经定义了一个 map 函数,可以根据需要对其进行修改,即可以修改为继承状态。

关于中间结果的构造

您以相反的顺序构造 wordsList,然后将其反转。我认为difference lists 做得更好,因为附加只是一个函数组合,因此附加只需要 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 []

可能不再需要此答案,但为了完整起见,我添加了它。

【讨论】:

  • writeFile 和 enumWrite 是同一个函数的两个名字吧?
【解决方案2】:

我认为这是对昨天Reading large file in 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

我现在看起来是线性的。

【讨论】:

  • 是的,但 ByteString.Lazy 包应该是懒惰的,至少我认为。无论如何,只要它是标准的,我不介意使用其他技术或库。我的编译器无法识别您向我提出的选项。我的个人版本是 6.12.1,而我必须使用的大学服务器上的版本是 6.8.2。如果我卸载 ghc 并重新安装最新版本,你认为我会得到正常的结果吗? PS:我昨天的问题是关于同一个项目,但另一个问题,你解决了,谢谢。
  • @Joel - -rtsopts 标志仅对 ghc-7 有用,所以不要使用它。 -O2 应该是标准的。
  • 再一次,你是对的。我按照你的解释,用内部缓冲区重写了我的整个 IO 库。事实上,我根本没有缓冲任何东西。我可能永远不会明白-Lazy IO- 是什么意思……但我会在考试中取得成功,因为我的haskell 压缩实用程序只比做同样事情的C 程序慢5 倍。您对 Chris Kuklewicz 非常有帮助,非常感谢您分享您的丰富知识。
  • 嗯,根据 GHC 文档:“目前,-O2 不太可能产生比 -O 更好的代码”(c)haskell.org/ghc/docs/7.0-latest/html/users_guide/…
猜你喜欢
  • 2017-02-03
  • 2022-11-14
  • 1970-01-01
  • 2011-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-11-01
  • 1970-01-01
相关资源
最近更新 更多