回想起来,整个问题可以归结为更简洁的东西。我正在寻找一个 Haskell 数据结构
- 看起来像一个列表
- 有 O(1) 查找
- 有 O(1) 元素替换或O(1) 元素追加(或前置......如果是这种情况,我可以反转我的索引查找)。我总是可以考虑其中一种或另一种来编写我以后的算法。
- 内存开销很小
我正在尝试构建图像文件解析器。文件格式是基本的 8 位彩色 ppm 文件,但我打算支持 16 位彩色文件以及 PNG 和 JPEG 文件。现有的 Netpbm 库,尽管有很多拆箱注释,但在尝试加载我使用的文件时实际上会消耗所有可用内存:
3-10张照片,最小的45MB,最大的110MB。
现在,我无法理解 Netpbm 代码中的优化,所以我决定自己尝试一下。这是一个简单的文件格式...
我首先决定无论文件格式是什么,我都将以这种格式存储未压缩的最终图像:
import Data.Vector.Unboxed (Vector)
data PixelMap = RGB8 {
width :: Int
, height :: Int
, redChannel :: Vector Word8
, greenChannel :: Vector Word8
, blueChannel :: Vector Word8
}
然后我写了一个解析器,它可以处理三个向量,如下所示:
import Data.Attoparsec.ByteString
data Progress = Progress {
addr :: Int
, size :: Int
, redC :: Vector Word8
, greenC :: Vector Word8
, blueC :: Vector Word8
}
parseColorBinary :: Progress -> Parser Progress
parseColorBinary progress@Progress{..}
| addr == size = return progress
| addr < size = do
!redV <- anyWord8
!greenV <- anyWord8
!blueV <- anyWord8
parseColorBinary progress { addr = addr + 1
, redC = redC V.// [(addr, redV)]
, greenC = greenC V.// [(addr, greenV)]
, blueC = blueC V.// [(addr, blueV)] }
在解析器结束时,我像这样构造 RGB8:
Progress{..} <- parseColorBinary $ ...
return $ RGB8 width height redC greenC blueC
像这样写的程序,加载这些 45MB 图像中的一个,将消耗 5GB 或更多的内存。如果我更改了Progress
so that redC
、greenC
和blueC
are all的定义!(Vector Word8)
,那么程序将保持在合理的内存范围内,但加载单个文件需要很长时间,以至于我不允许它实际完成。最后,如果我用标准列表替换这里的向量,我的内存使用量会回升到每个文件 5GB(我假设......我实际上在点击之前用完了空间),加载时间大约为 6 秒. Ubuntu 的预览应用程序一旦启动,就会几乎立即加载和呈现文件。
根据每次对 V.// 的调用实际上每次都完全复制向量的理论,我尝试切换到Data.Vector.Unboxed.Mutable
,但是......我什至无法进行类型检查。文档不存在,理解数据类型在做什么也需要与多个其他库进行斗争。而且我什至不知道它是否能解决问题,所以我什至都不愿意尝试。
基本问题实际上非常简单:
我如何在不使用大量内存的情况下快速读取、保留并可能操作非常大的数据结构?我发现的所有示例都是关于临时生成大量数据,然后尽快将其删除。
原则上,我希望最终表示是不可变的,但我不太关心是否必须使用可变结构才能到达那里。
为了完整起见,完整的代码(BSD3 许可)位于https://bitbucket.org/savannidgerinel/photo-tools的 bitbucket 上。该分支包含一个严格版本的解析器,performance
可以通过快速更改Progress
.Codec.Image.Netpbm
运行性能测试
ulimit -Sv 6000000 -- set a ulimit of 6GB, or change to whatever makes sense for you
cabal build
dist/build/perf-test/perf-test +RTS -p -sstderr