我需要使用自定义编码函数(我有)序列化大量值.我已经完成了它并且它可以工作,但我还想让它计算有多少值被序列化并写入磁盘,同时仍使用相对恒定的内存量(即它不需要保留整个输入列表,因为它变得非常大).
没有保持计数的要求,二元,谷物和火焰生成器都可以工作(使用等效的B.writeFile“foo”.runPut.mapM_ encodeValue);但无论我尝试使用这些库中的任何一个,似乎生成的ByteString在内存中保持不变,直到它完成而不是一旦块可用就开始写入磁盘(即使从闪存中使用toByteStringIO
) -builder).
这是一个最小的例子,展示了我一直在尝试做的事情:
import Data.Binary
import Data.Binary.Put
import Control.Monad(foldM)
import qualified Data.ByteString.Lazy as B
main :: IO ()
main = do let ns = [1..10000000] :: [Int]
(count,b) = runPutM $foldM (\ c n -> c `seq` (put n >> return (c+1))) (0 :: Int) ns
B.writeFile "testOut" b
print count
当使用RTS -hy编译和运行时,结果是由ByteString值支配的几乎三角形图.
我到目前为止找到的唯一解决方案(我不是很喜欢)是使用B.appendFile在IO中循环(直接或使用foldM),而不是在Put中或直接构建Builder值,对我来说似乎并不优雅.有没有更好的办法?
最佳答案 我有点惊讶toByteStringIO不起作用,希望有更熟悉该库的人会提供答案.
话虽这么说,每当我想将流处理与IO动作混合时,我通常会发现迭代是最优雅的解决方案.这是因为它们允许精确控制处理和保留的数据量,以及将流方面与其他任意IO操作相结合.关于hackage有several iteratee implementations;这个例子是“iteratee”,因为它是我最熟悉的那个.
import Data.Binary.Put
import Control.Monad
import Control.Monad.IO.Class
import qualified Data.ByteString.Lazy as B
import Data.ByteString.Lazy.Internal (defaultChunkSize)
import Data.Iteratee hiding (foldM)
import qualified Data.Iteratee as I
main :: IO ()
main = do
let ns = [1..80000000] :: [Int]
iter <- enumPureNChunk ns (defaultChunkSize `div` 8)
(joinI $serializer $writer "testOut")
count <- run iter
print count
serializer = mapChunks ((:[]) . runPutM . foldM
(\ !cnt n -> put n >> return (cnt+1)) 0)
writer fp = I.foldM
(\ !cnt (len,ck) -> liftIO (B.appendFile fp ck) >> return (cnt+len))
0
这有三个部分. writer是“iteratee”,即数据使用者.它将每个数据块写为接收数据并保持长度的运行计数. serializer是一个流变换器a.k.a.“enumeratee”.它接受[Int]类型的输入块并将其序列化为类型为[(Int,B.ByteString)]的流(元素数,bytestring).最后,enumPureNChunk是“枚举器”,它在这种情况下从输入列表产生一个流.它从输入中获取足够的元素以填充单个惰性字节串块(我在64位上,对于32位系统除以4),然后将它们写入磁盘以便它们可以进行GC.