【问题标题】:Unexpected memory growth with Control.Monad foldMControl.Monad foldM 意外的内存增长
【发布时间】:2016-01-16 19:01:18
【问题描述】:

我有以下代码,它已被剥离,我认为尽可能少,但有一些非常奇怪的行为。

代码由两个源文件组成: 一、定义一些数据:

module MyFunction where

data MyFunction =
    MyFunction {
        functionNumber :: Int,
        functionResult :: IO String
        }

makeMyFunction :: Show a => Int -> IO a -> MyFunction
makeMyFunction number result = MyFunction {
    functionNumber = number,
    functionResult = result >>= return . show }

另一个是Main:

module Main (main) where

import System.CPUTime (getCPUTime)
import Data.List (foldl')
import Data.Foldable (foldlM)
import Control.Monad (foldM)
import MyFunction

exampleFunction = do
    --let x = foldl' (\a b -> a `seq` (a + b)) 0 [1..20000000]      -- This works
    --x <- foldlM (\a b -> a `seq` return (a + b)) 0 [1..20000000]  -- This works (*)
    x <- foldM (\a b -> a `seq` return (a + b)) 0 [1..20000000]    -- This doesn't
    print x
    return ()

runFunction fn = do
    result <- functionResult fn
    duration <- getCPUTime
    if result /= "()"
        then putStrLn ""
        else return ()
    putStrLn (show (fromIntegral duration / (10^9)) ++ "ms")
    return fn

main = do
    runFunction (makeMyFunction 123 exampleFunction)
    return ()

上述代码(使用 GHC 7.10.3 编译,堆栈 1.0.0 和默认标志)内存使用量迅速增加(超过 1GB),通常需要 3.3 秒。

如果我对代码进行更改,例如:

  • 使用问题行的注释替代方法之一
  • runFunction中取出任意一行

内存使用量将保持最小,只需大约 1 秒。

我认为最令我惊讶的一个功能是用foldlM(据我所知foldM = foldlM)替换foldM 可以解决问题。

还对我看不到的代码进行更改与问题代码行有任何关系也可以解决问题。例如删除最后一个 putStrLn。

另一个奇怪的地方是,如果我将 MyFunction 模块合并到 Main 模块中,虽然它不能解决问题,但它实际上会导致 foldlM 使用过多的内存作为 foldM

在这个来自的真实代码中,我有大量的exampleFunctions,并且有明显更多的Main 代码,而且我经常遇到这种来自函数的无法解释的内存使用情况,通常可以用某种巫术来解决。

我正在寻找该行为的解释。如果我知道为什么会发生这种情况,我可以考虑避免它。这可能是编译器问题,还是我的误解?

(*) 我强调了导致 foldlM 出现相同内存增长的次要问题。

【问题讨论】:

  • 在模块之间移动东西会强烈影响行为这一事实表明,GHC 的内联器也参与其中。最有可能的是,通过内联实现的某些转换要么帮助你,要么伤害你。
  • 一个不相关的问题是您在 exampleFunction 中的算术默认为 Integer,这将非常慢。开启-Wall
  • @jberryman exampleFunction 仅作为示例 - 显示内存增加。
  • 查看使用 ghc --make -fforce-recomp -Wall -O1 -ddump-simpl -dsuppress-all -ddump-inlinings -ddump-rule-firings -ddump-to-file 和 gvimdiff 编译 foldM 和 foldlM 版本的 *.dump-simpl 和 *.dump-inlinings 文件。在您的慢版本列表中没有被融合(您可以使用 grep 查找“:”)。如果你喜欢做这种事情,你可以试着弄清楚为什么会发生这种情况,但我认为最好报告一个错误。
  • @pticawr 是的,我想是的;我在你的慢版本中看到的主要区别是中间列表没有融合,这都是库级别的问题:它由重写规则和 INLINE pragma 控制(这可能非常脆弱,而且难以理解)。我想作者想知道foldlM 没有表现出与foldM 相同的行为w/r/t 内联和优化。

标签: haskell


【解决方案1】:

这是来自Foldable.hs (ghc) 的foldlM

-- | Monadic fold over the elements of a structure,
-- associating to the left, i.e. from left to right.
foldlM :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
foldlM f z0 xs = foldr f' return xs z0
  where f' x k z = f z x >>= k

foldM 来自Monad.hs

foldM          :: (Foldable t, Monad m) => (b -> a -> m b) -> b -> t a -> m b
{-# INLINEABLE foldM #-}
{-# SPECIALISE foldM :: (a -> b -> IO a) -> a -> [b] -> IO a #-}
{-# SPECIALISE foldM :: (a -> b -> Maybe a) -> a -> [b] -> Maybe a #-}
foldM          = foldlM

我将这些定义放在一个单独的模块 Test 中,并测试了使用和不使用 INLINEABLE / SPESIALISE 行的执行情况。不管是什么原因,省略 SPECIALIZE 指令有帮助,执行时间和内存使用与 foldlM 一样。

再挖一点点,去掉线

{-# SPECIALISE foldM :: (a -> b -> IO a) -> a -> [b] -> IO a #-}

影响最大。

【讨论】:

  • 原因是标记为SPECIALIZE 的东西是专门针对给定类型编译的,当在使用站点看到该类型时,使用专门的版本; GHC(通常?)考虑内联。对于像foldM 这样简单的东西,专业化似乎几乎肯定比一般的内联更糟糕,所以应该删除那些编译指示。
  • @dfeuer ; J.J. :虽然删除专业化解决了一个问题(使用上述源文件时)。您能解释一下为什么在将源文件合并为一个时会出现问题,但 foldM 和 foldlM 都显示内存使用量增长?
  • @pticawr 没有像 -O1 这样的优化标志?
  • @pticawr,内联是一只可怕的毛茸茸的野兽。我似乎记得标记为INLINEABLE 的东西在定义它们的模块中not 内联,但我可能记错了。内联是可怕而微妙的,因为它基于各种启发式方法,旨在实现尽可能多的有用转换,而不会使代码大小爆炸。它通常有效,但当它不起作用时,你就输了。列表融合之类的东西往往特别敏感。
  • 禁用优化 (-O0) 并不能解决这个问题 - 唯一的影响是它需要两倍的时间。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2018-07-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-02-14
相关资源
最近更新 更多