在Haskell中折叠生产者/分析器时的空间爆炸

时间:2014-05-19 13:26:53

标签: haskell memory stream reduce haskell-pipes

假设我有一个这样的模块:

module Explosion where

import Pipes.Parse (foldAll, Parser, Producer)
import Pipes.ByteString (ByteString, fromLazy)
import Pipes.Aeson (DecodingError)
import Pipes.Aeson.Unchecked (decoded)
import Data.List (intercalate)
import Data.ByteString.Lazy.Char8 (pack)
import Lens.Family (view)
import Lens.Family.State.Strict (zoom)

produceString :: Producer ByteString IO ()
produceString = fromLazy $ pack $ intercalate " " $ map show [1..1000000]

produceInts :: 
    Producer Int IO (Either (DecodingError, Producer ByteString IO ()) ())
produceInts = view decoded produceString

produceInts' :: Producer Int IO ()
produceInts' = produceInts >> return ()

parseBiggest :: Parser ByteString IO Int
parseBiggest = zoom decoded (foldAll max 0 id)

'produceString'函数是一个bytestring生成器,我关心的是在它上面折叠一个解析来产生某种结果。

以下两个程序通过将其解析为一系列JSON int来显示解决在bytestring中查找最大值的问题的不同方法。

计划1:

module Main where

import Explosion (produceInts')
import Pipes.Prelude (fold)

main :: IO ()
main = do
    biggest <- fold max 0 id produceInts'
    print $ show biggest

计划2:

module Main where

import Explosion (parseBiggest, produceString)
import Pipes.Parse (evalStateT)

main :: IO ()
main = do
    biggest <- evalStateT parseBiggest produceString
    print $ show biggest

不幸的是,当我对它们进行分析时,这两个程序总共消耗了大约200MB的内存,我希望使用流式解析器可以解决这个问题。第一个程序花费大部分时间和内存(> 70%)来自Lens.Family的(^.),而第二个程序花费在fmap,由zoom从Lens.Family调用.State.Strict。使用图如下。两个程序都花费大约70%的时间来进行垃圾收集。

我做错了吗? Prelude函数max不够严格吗?我无法判断库函数是否错误,或者我是否使用了库错误! (可能是后者。)

为了完整性,here's a git repo你可以克隆并运行cabal install,如果你想看到我正在谈论的第一手资料,这里是两个程序的内存使用情况:

memory usage of program 1 memory usage of program 2

1 个答案:

答案 0 :(得分:5)

在单个yield中包含严格的字节字符串并不会使其变得懒惰。您必须产生较小的块以获得任何流式传输行为。

编辑:我发现了错误。 pipes-aeson在内部使用如下定义的consecutively函数:

consecutively parser = step where
    step p0 = do
      (mr, p1) <- lift $
         S.runStateT atEndOfBytes (p0 >-> PB.dropWhile B.isSpaceWord8)
      case mr of
         Just r  -> return (Right r)
         Nothing -> do
            (ea, p2) <- lift (S.runStateT parser p1)
            case ea of
               Left  e -> return (Left (e, p2))
               Right a -> yield a >> step p2

有问题的行是PB.dropWhile的行。这会增加与解析元素数量成比例的二次爆炸。

在每次解析之后,穿过此计算的管道会在其下游累积新的cat管道。因此,在N解析后,您将获得N cat个管道,这会为每个已解析的元素添加O(N)开销。

我已创建a Github issue来解决此问题。 {1}由Renzo维护,他之前已解决过这个问题。

编辑:我已提交pull request来解决第二个问题(您需要使用pipes-aeson来表示延迟字节串)。现在,该程序以两个版本的5 KB常量空间运行:

enter image description here

enter image description here

相关问题