【问题标题】:Where is the memory leak in using StateT s IO a?使用 StateT s IO a 的内存泄漏在哪里?
【发布时间】:2018-12-03 10:57:35
【问题描述】:

意图: 学习 Haskell 的小应用程序:下载 wikipedia-article,然后下载其中链接的所有文章,然后下载链接的所有文章,依此类推……直到指定的递归深度达到。结果保存到文件中。

方法:使用StateT 跟踪下载队列、下载文章和更新队列。我递归地建立一个列表IO [WArticle],然后打印它。

问题:在进行分析时,我发现使用的总内存与下载的文章数量成正比。

分析:根据文献,我相信这是一个懒惰和/或严格的问题。 BangPatterns 减少了内存消耗,但没有解决比例问题。此外,我知道所有文章都是在文件输出开始之前下载的。

可能的解决方案:

1) 函数getNextNode :: StateT CrawlState IO WArticle(下)已经有IO。一种解决方案是只在其中写入文件并仅返回状态。这意味着文件被写入非常小的块。感觉不是很 Haskell..

2) 让函数buildHelper :: CrawlState -> IO [WArticle](下)返回[IO WArticle]。虽然我不知道如何重写该代码,并且在 cmets 中被建议不要使用它。

这些提议的解决方案是否比我认为的更好,或者是否有更好的替代方案?

import GetArticle (WArticle, getArticle, wa_links, wiki2File) -- my own
type URL = Text

data CrawlState =
     CrawlState  ![URL]       ![(URL, Int)]
          --    [Completed]    [(Queue, depth)]
-- Called by user
buildDB :: URL -> Int -> IO [WArticle]
buildDB startURL recursionDepth = buildHelper cs
    where cs = CrawlState [] [(startURL, recursionDepth)]

-- Builds list recursively
buildHelper :: CrawlState -> IO [WArticle]
buildHelper !cs@(CrawlState _ queue) = {-# SCC "buildHelper" #-}
  if null queue
    then return []
    else do
      (!article, !cs') <- runStateT getNextNode cs
      rest <- buildHelper cs'
      return (article:rest)

-- State manipulation
getNextNode :: StateT CrawlState IO WArticle
getNextNode = {-# SCC "getNextNode" #-} do
  CrawlState !parsed !queue@( (url, depth):queueTail ) <- get
  article <- liftIO $ getArticle url
  put $ CrawlState (url:parsed) (queueTail++ ( if depth > 1
          then let  !newUrls  = wa_links article \\ parsed
                    !newUrls' = newUrls          \\ map fst queue
                    in zip newUrls' (repeat (depth-1))
          else []))
  return article

startUrl = pack "https://en.wikipedia.org/wiki/Haskell_(programming_language)"
recursionDepth = 3

main :: IO ()
main =  {-# SCC "DbMain" #-}
  buildDB startUrl recursionDepth
   >>= return . wiki2File
   >>= writeFile "savedArticles.txt"

完整代码https://gitlab.com/mattias.br/sillyWikipediaSpider。当前版本仅限于下载每个页面的前八个链接以节省时间。在不改变它的情况下,以大约 600 MB 的堆使用量下载 55 个页面。

感谢您的帮助!

【问题讨论】:

  • 您的假设是不正确的:IO [WArticle] 在评估列表的每个元素时仍然很懒惰。 IO 不强制要求严格。这方面的一个例子是执行代码do {a &lt;- return [undefined,1]; print (a !! 1)}。长话短说,使用[IO Article]s 只会让自己的生活更加艰难。
  • 不是一个直接的答案,但conduit 和类似的库恰恰提供了该功能:处理具有更明确的资源控制的大型数据“列表”。
  • @AJFarmar,谢谢你的回答。所以这个问题应该改写为“为什么懒惰被打破了?”?由于do {db &lt;- buildDB url 3} 打印出跟踪消息以立即下载所有文章(跟踪消息打印在getArticle 中,此处未引用)。

标签: haskell lazy-evaluation state-monad io-monad


【解决方案1】:

2) 在这种情况下,我想要 [IO WArticle] 吗?

不完全是。问题是某些IO WArticle 操作依赖于先前操作的结果:指向未来页面的链接驻留在先前获得的页面中。 [IO Warticle] 无法提供这一点:纯粹是因为您始终可以在列表中找到一个动作而无需执行之前的动作。

我们需要的是一种“效果列表”,让我们逐条提取文章,逐步执行需要的效果,而不是强迫我们一次性完成列表。

有几个库可以提供这些类型的“有效列表”:streamingpipesconduit。他们定义了 monad 转换器,这些转换器扩展了基本 monad,能够在返回最终结果之前yield 中间值。通常最终结果的类型与产生的值不同;它可能只是单位()

注意:这些库的FunctorApplicativeMonad 实例与纯列表的相应实例不同。 Functor instances 映射 resulting 最终值,而不是产生的中间值。为了映射产生的值,他们提供了separate functions。并且 Monad 实例 sequence 有效列表,而不是尝试所有组合。要尝试所有组合,他们提供separate functions

使用streaming 库,我们可以将buildHelper 修改为如下内容:

import Streaming
import qualified Streaming.Prelude as S

buildHelper :: CrawlState -> Stream (Of WArticle) IO ()
buildHelper !cs@(CrawlState _ queue) = 
  if null queue
    then return []
    else do (article, cs') <- liftIO (runStateT getNextNode cs)
            S.yield article
            buildHelper cs'

然后我们可以使用类似mapM_(来自Streaming.Prelude,而不是来自Control.Monad!)之类的函数来处理生成的文章。

【讨论】:

  • 干杯!我会调查一下。当您输入答案时,我一定更新了问题。对不起!我会建议进行修改以提高未来读者的可读性。
  • Streaming.Prelude.toHandle 似乎适合写入文件。所以db :: Stream (Of WArticle) IO () 我做了main = do {handle &lt;- System.IO.openFile "outfile.txt" WriteMode; S.toHandle handle $ S.show db。它仍然会消耗大量 RAM,但无论下载多少文章,它都不会超过 450 MB。我想那是因为我有空闲的 RAM。
【解决方案2】:

在 danidiaz 的回答的基础上添加进一步的解释和代码。这是最终代码:

import Streaming
import qualified Streaming.Prelude as S
import System.IO (IOMode (WriteMode), hClose, openFile)

buildHelper :: CrawlState -> Stream (Of WArticle) IO ()
buildHelper cs@( CrawlState _ queue ) = 
  if null queue
    then return ()
    else do
      (article, cs') <- liftIO (runStateT getNextNode cs)
      S.yield article
      buildHelper cs'

main :: IO ()
main = do outFileHandle <- openFile filename WriteMode
          S.toHandle outFileHandle  . S.show . buildHelper $
              CrawlState [] [(startUrl, recursionDepth)]
          hClose outFileHandle

outFileHandle 是一个常用的文件输出句柄。

S.toHandle 接受一个字符串流并将它们写入指定的句柄。

S.show 在流上映射show :: WArticle -&gt; String

一个优雅的解决方案,它创建一个惰性流,即使它是由一系列 IO 操作(即下载网站)产生的,并在结果可用时将其写入文件。在我的机器上,它在执行期间仍然使用大量内存(相对于任务),但从未超过 450 MB。

【讨论】:

  • 你可以用System.IO.withFile代替openFile/hClose;它使您不必记住关闭手柄。使用String——它是一个字符的链表——可能效率低下,因为某些使用像BytestringText 这样的打包表示会更好。此外,您可以将-M 选项传递给运行时以设置最大堆大小:downloads.haskell.org/~ghc/latest/docs/html/users_guide/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-10-25
  • 1970-01-01
  • 2012-04-09
  • 1970-01-01
相关资源
最近更新 更多