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

mnt*_*123 1 binary performance haskell bytestring

我想在Haskell中编写一个类似hexdump的程序.我编写了以下程序,我很高兴它可以工作并提供所需的输出,但它非常慢且效率低.它是根据这个答案中给出的程序改编的.

我用一个示例文件运行程序处理少于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 ""
Run Code Online (Sandbox Code Playgroud)

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

*Main> main
before time: 
23254.13701s
after time: 
23313.381806s
Run Code Online (Sandbox Code Playgroud)

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

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

Mat*_*hid 10

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

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

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

首先,没有名为"byte"的类型.调用你要查找的类型Word8(如果你想要一个无符号的 8位整数)或Int8(如果你想要一个有符号的 8位整数 - 你可能不需要).还有一些类型,比如Word16,Word32,Word64,你需要导入Data.Word才能获得它们.同样,Int16,Int32Int64Data.Int.在IntInteger类型自动进口的,所以你不需要做什么特别的那些.

A ByteString基本上是一个字节数组.甲[Word8],在另一方面,是可以或可以不还计算各个字节的单链接列表-多效率较低,但更灵活.

如果从字面上所有你想要做的是应用转换到每一个字节,独立于任何其他字节,那么ByteString包提供了一个map功能,将做到这些:

map :: (Word8 -> Word8) -> ByteString -> ByteString
Run Code Online (Sandbox Code Playgroud)

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

那么它是怎样工作的?那么,ByteString图书馆有一个功能

readFile :: FilePath -> IO ByteString
Run Code Online (Sandbox Code Playgroud)

看起来像它读取整个文件转换成一个巨大的ByteString内存.但事实并非如此.这是一个技巧.实际上它只是检查文件是否存在,并打开它进行读取.当您尝试使用ByteString,在后台将文件无形被你处理它读入内存中.这意味着你可以这样做:

main = do
  bin <- readFile "in_file"
  writeFile "out_file" (map my_function bin)
Run Code Online (Sandbox Code Playgroud)

这将读取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.
Run Code Online (Sandbox Code Playgroud)

请注意,因为一个字节变为两个十六进制数字,所以我使用的是ByteString版本concatMap,它允许my_function返回整数ByteString而不是单个字节.

  • @ mntk123,关于`apendFile`的第二个问题,当函数采用`FilePath`而不是打开的文件句柄时,你可以推断它使用提供的`FilePath`打开文件,写入它,然后关闭它下.(这几乎适用于所有`System.IO`函数) (2认同)