在Haskell中处理大文件的IO:性能问题

6

我正在尝试使用Haskell处理大文件。我希望能够逐字节浏览输入文件,并逐字节生成输出文件。当然,我需要IO缓冲区的块大小合理(几KB)。但我做不到,所以需要您的帮助。

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库。
说明:这是一个大学项目。我不是在尝试复制文件。doSomeStuff函数应执行我必须自定义的压缩/解压缩操作。

我刚刚尝试了一下不使用pack和unpack,直接在字节串上操作,结果甚至更长,而且5MB和10MB之间仍然有很大的差异。 - Joel
也许你可以在某个地方发布一个完整的工作示例,演示代码中存在的问题? - Ed'ka
事实上,对于一个5MB的文件来说,速度还可以,不算太慢。但当我达到10MB的限制时,输入输出变得非常缓慢。我需要进行的转换是项目特定的位操作。由于这是一个特定的大学项目,我不能使用库来完成这些操作。我必须自己对字节进行位操作。 - Joel
我知道对于5MB来说它很慢,我想要另一种可以接受的性能解决方案。我不知道其他的。对于mapAccumL函数,我不能使用它,因为我没有将字节映射到字节。我正在压缩和解压缩,输入文件中的1字节会被映射到输出中的一定数量的位数。所有的压缩工作都已经很好地完成了,并且非常快。我的问题就是我在问题中明确写出的问题:从输入文件中浏览字节并将字节写入输出文件。谢谢。 - Joel
1
cabal unpack blaze-builder -- 现在你已经获取了源代码,可以直接将需要的文件移动到你的存储库中。 - sclv
显示剩余7条评论
2个回答

8

对于分块输入处理,我会使用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进行写入。

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

如您所见,步骤函数会将字节串的块附加到目标文件中。这些块的类型为Stream BS.Bytestring,其中Stream被定义为:

data Stream a = Chunks [a] | EOF

在EOF步骤终止时,会产生()。
要对此进行更详细的阅读,我个人推荐Michael Snoymans的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,然后将其反转。我认为 差异列表 做得更好,因为附加只需要 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是同一个函数的两个名称,对吗? - runeks

1

我猜这是昨天在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
@Joel - -rtsopts 标志只有在使用 ghc-7 时才有用,所以不要加上它。 -O2 应该是标准的。 - John L
再次感谢您的正确指导。我按照您的解释重新编写了我的IO库,并使用内部缓冲区。事实上,我根本没有进行任何缓冲处理。也许我永远无法理解“惰性IO”的含义...但是,我的Haskell压缩工具仅比执行相同任务的C程序慢5倍,我将在考试中取得成功。Chris Kuklewicz,您非常有帮助,感谢您分享您的伟大知识。 - Joel
根据 GHC 文档,目前 "-O2" 不太可能比 "-O" 产生更好的代码。(引自 http://www.haskell.org/ghc/docs/7.0-latest/html/users_guide/options-optimise.html#optimise-pkgs) - Ed'ka

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