Control.Monad foldM意外的内存增长

pti*_*awr 9 haskell

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

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

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 }
Run Code Online (Sandbox Code Playgroud)

另一个是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 ()
Run Code Online (Sandbox Code Playgroud)

上面的代码(使用GHC 7.10.3编译,堆栈1.0.0带有默认标志)内存使用量快速增加(超过1GB),通常需要3.3秒.

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

  • 使用问题行的注释替代方案之一
  • 取出任何一行 runFunction

内存使用量将保持最小,仅需1秒左右.

一个功能,我认为这是最令人惊讶的对我来说是替换foldM用foldlM(这是据我所知foldM = foldlM)解决了这个问题.

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

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

在它来自的真实代码中,我有大量exampleFunction的Main代码,并且代码明显增多,而且我经常会遇到函数中这种无法解释的内存使用情况,这通常可以通过某种伏都教来解决.

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

(*)我已经强调了使用foldlM导致相同内存增长的次要问题.

J.J*_*ala 3

这是foldlM来自Foldable.hs(ghc)

-- | 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
Run Code Online (Sandbox Code Playgroud)

和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
Run Code Online (Sandbox Code Playgroud)

我将这些定义放置到一个单独的模块 Test 中,并测试了带或不带 INLINEABLE / SPESIALISE 行的执行情况。无论原因是什么,省略 SPECIALIZE 指令都会有所帮助,并且执行时间和内存使用情况与使用foldlM 类似。

进一步挖掘后,移除线

{-# SPECIALISE foldM :: (a -> b -> IO a) -> a -> [b] -> IO a #-}
Run Code Online (Sandbox Code Playgroud)

影响最大。

  • 原因是标记为“SPECIALIZE”的内容是专门针对给定类型进行编译的,当在使用站点上看到该类型时,就会使用专门的版本;GHC(通常?)*不*考虑内联。对于像“foldM”这样简单的东西,专业化似乎几乎肯定比一般的内联更糟糕,因此应该删除这些编译指示。 (3认同)