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

how to improve this very slow and inefficient Haskell program to process binary files byte by byte?

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

中给出的程序

我运行一个sample file的程序,处理那个不到1MB的文件大约需要1分钟。标准的 Linux hexdump 程序可以在不到一秒的时间内完成这项工作。我想在程序中做的就是读取->处理->写入字节串中的所有单个字节。

这里是问题 - 如何有效地 read/process/write 字节串(逐字节,即不使用任何其他函数,如 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 书籍找到解决方案,我一开始就不会问这个问题。

抱歉看起来 运行t,但与许多其他语言相比,使用 Haskell 的体验令人沮丧,尤其是在像 Haskell 那样做简单的 IO 时几乎没有包含 小型完整工作示例 的 IO 相关文档。

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

我不是 100% 确定您要在这里实现的目标(即,尝试了解东西的工作原理与尝试使特定程序工作),所以我不确定如何最好地回答你的问题。

如果这是您第一次涉足 Haskell,那么从 I/O-centric 开始可能不是个好主意。在担心如何实现高性能 I/O 之前,你最好先学习语言的其余部分。也就是说,让我尝试回答您的实际问题...

首先,没有名为 "byte" 的类型。您要查找的类型称为 Word8(如果您想要 unsigned 8 位整数)或 Int8(如果您想要 signed 8 位整数——你可能不知道)。还有Word16Word32Word64等类型;您需要导入 Data.Word 才能获取它们。同样,Int16Int32Int64 住在 Data.IntIntInteger 类型是自动导入的,所以你不需要为它们做任何特殊的事情。

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

如果字面上 all 你想要做的是对每个字节应用一个转换,独立于任何其他字节,那么 ByteString 包提供了一个 map 函数将执行此操作:

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

如果您只是 想从一个文件读取并写入另一个文件,您可以使用所谓的"lazy I/O" 来实现。这是一个巧妙的躲避,库为您处理所有 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.

请注意,因为 一个 字节变成 两个 十六进制数字,所以我使用了 [=42] 的 ByteString 版本=],它允许 my_function 到 return 整个 ByteString 而不是一个字节。