如何改进这个非常慢和低效的Haskell程序来逐字节处理二进制文件?

时间:2015-08-30 08:28:14

标签: performance haskell binary bytestring

我正在尝试在Haskell中编写类似hexdump的程序。我编写了以下程序,我很高兴它可以工作并提供所需的输出,但它非常慢且效率低。它改编自this answer中的程序。

我使用sample file运行程序,处理少于1MB的文件大约需要1分钟。标准的Linux hexdump程序可以在不到一秒的时间内完成工作。我想在程序中执行的操作是read-> process->在bytestring中写入所有单个字节。

这是一个问题 - 如何有效地读取/处理/写入字节串(逐字节,即不使用任何其他函数,如getWord32le,如果需要的话)?我想对每个单独的字节进行算术和逻辑运算,不一定在Word32le或像这样的一组字节。我没有找到像Byte这样的数据类型。

无论如何,这是我写的代码,它在ghci(版本7.4)上成功运行 -

module Main where

import Data.Time.Clock
import Data.Char
import qualified Data.ByteString.Lazy as BIN
import Data.ByteString.Lazy.Char8
import Data.Binary.Get
import Data.Binary.Put
import System.IO
import Numeric (showHex, showIntAtBase)

main = do
  let infile = "rose_rosebud_flower.jpg"
  let outfile = "rose_rosebud_flower.hex"
  h_in  <- openFile infile ReadMode
  System.IO.putStrLn "before time: "
  t1 <- getCurrentTime >>= return . utctDayTime
  System.IO.putStrLn $ (show t1)
  process_file h_in outfile
  System.IO.putStrLn "after time: "
  t2 <- getCurrentTime >>= return . utctDayTime
  System.IO.putStrLn $ (show t2)
  hClose h_in

process_file h_in outfile = do 
  eof <- hIsEOF h_in
  if eof 
      then return ()
      else do  bin1 <- BIN.hGet h_in 1
               let str = (Data.ByteString.Lazy.Char8.unpack) bin1
               let hexchar = getHex str
               System.IO.appendFile outfile hexchar
               process_file h_in outfile

getHex (b:[]) = (tohex $ ord b) ++ " " 
getHex _ = "ERR "

tohex d = showHex d ""

当我在ghci上运行时,我得到了

*Main> main
before time: 
23254.13701s
after time: 
23313.381806s

请提供修改后的(但工作完整的)代码作为答案,而不仅仅是某些功能的名称列表。此外,不提供使用jpeg或其他图像处理库的解决方案,因为我对图像处理不感兴趣。我使用jpeg图像作为示例非文本文件。我只想逐字节处理数据。也不要提供其他站点的链接(特别是Haskell站点上的文档(或缺少它))。我无法理解bytestring的文档以及在Haskell网站上编写的许多其他软件包,他们的文档(在大多数情况下只是在页面上收集的类型签名)似乎只适用于已经理解了大部分内容的专家。如果我可以通过阅读他们的文档甚至广告(现实世界的haskell)RWH书来找出解决方案,我首先不会问这个问题。

对于看似咆哮感到抱歉,但与其他许多语言相比,Haskell的使用体验令人沮丧,特别是当使用小型完整工作示例对Haskell IO相关文档进行简单的IO时几乎没有。

1 个答案:

答案 0 :(得分:10)

您的示例代码一次读取一个字节。这几乎可以保证很慢。更好的是,它读取1字节ByteString,然后立即将其转换为列表,否定ByteString的所有好处。最重要的是,它通过打开文件,附加单个字符,然后再次关闭文件的稍微奇怪的方法写入输出文件。因此,对于写出的每个单独的十六进制字符,文件必须完全打开,缠绕到最后,附加一个字符,然后刷新到磁盘并再次关闭。

我并不是100%肯定你在这里想要实现的目标(即,试图了解其中的东西是如何工作的,而不是试图让特定的程序工作),所以我不确定如何最好地回答你的问题。

如果这是您第一次涉足Haskell,从以I / O为中心的事情开始可能是一个坏主意。在担心如何进行高性能I / O之前,最好先学习其余的语言。那就是说,让我试着回答你的实际问题......

首先,没有名为&#34; byte&#34;的类型。您正在寻找的类型称为Word8(如果您想要无符号 8位整数)或Int8(如果您想要签名< / em> 8位整数 - 你可能不会这样做。还有Word16Word32Word64等类型;你需要导入Data.Word来获取它们。同样,Int16Int32Int64位于Data.IntIntInteger类型会自动导入,因此您不需要为这些类型执行任何特殊操作。

ByteString基本上是一个字节数组。另一方面,[Word8]是单个链接的单个字节列表,可能会或可能不会被计算 - 效率低得多,但更灵活。

如果你想要做的字面 all 是将变换应用于每个字节,与任何其他字节无关,那么ByteString包提供了一个map函数,做到这一点:

map :: (Word8 -> Word8) -> ByteString -> ByteString

如果只是想要从一个文件中读取并写入另一个文件,则可以使用所谓的&#34;懒惰的I / O&#34;来实现。这是一个整洁的闪避,图书馆为您处理所有的I / O块。虽然它有一些讨厌的陷阱;基本上围绕它很难确切知道什么时候输入文件将被关闭。对于简单的情况,这并不重要。对于更复杂的情况,确实如此。

那它是如何运作的?好吧,ByteString库有一个函数

readFile :: FilePath -> IO ByteString

看起来像它将整个文件读入内存中的巨型​​ByteString。但它并没有。这是一个技巧。实际上它只是检查文件是否存在,并打开它进行读取。当您尝试使用 ByteString时,在后台将文件在处理时无形地读入内存。这意味着你可以这样做:

main = do
  bin <- readFile "in_file"
  writeFile "out_file" (map my_function bin)

这将读取in_file,将my_function应用于文件的每个字节,并将结果保存到out_file,自动以足够大的块进行I / O以提供良好的性能,但从不在RAM中同时保存多个块。 (my_function部分必须具有类型Word8 -> Word8。)因此,编写起来非常简单,并且应该非常快。

如果您不想阅读整个文件,或想要以随机顺序访问该文件,或者任何类似的复杂文件,事情会变得很有趣。我告诉我要查看pipes库,但我个人从未使用它。

为了一个完整的工作范例:

module Main where

import Data.Word
import qualified Data.ByteString.Lazy as BIN
import Numeric

main = do
  bin <- BIN.readFile "in_file"
  BIN.writeFile "out_file" (BIN.concatMap my_function bin)

my_function :: Word8 -> BIN.ByteString
my_function b =
  case showHex b "" of
    c1:c2:_ -> BIN.pack [fromIntegral $ fromEnum $ c1 , fromIntegral $ fromEnum $ c2]   -- Get first two chars in hex string, convert Char to Word8.
    c2:_    -> BIN.pack [fromIntegral $ fromEnum $ '0', fromIntegral $ fromEnum $ c2]   -- Only one digit. Assume first digit is zeor.

请注意,因为一个字节变为两个十六进制数字,所以我使用了ByteStringconcatMap,这允许{ {1}}返回整个my_function而不是单个字节。