对类型同义词使用“type”会导致内存泄漏吗?

时间:2014-04-14 15:41:47

标签: haskell memory-leaks type-systems

我有以下代码作为我的热循环。

{-# LANGUAGE BangPatterns #-}
module Simulation.Simulator where

import Simulation.Common ()
import System.Random (RandomGen)
import Control.Monad.Random (Rand)

simulateUntil :: 
        (RandomGen g) =>
        Int                             ->      --Number of simulation steps
        a                               ->      --Initial model
        (a -> Rand g Bool)              ->      --Function to check if simulation should end
        (Float -> a -> Rand g a)        ->      --Update model one step
        Rand g a 
simulateUntil !num !model !done !update = do
        !stop <- done model
        if stop then return model 
        else do updateM <- update (1 / fromIntegral num) model
                simulateUntil num updateM done update

为了尝试使这个循环更具可读性并且与我的其余代码更内联,我在Simulation.Common代码中添加了一个类型同义词:

type SearchEnv a = (RandomGen g) => Rand g a

然后我改变了上面的循环来使用这个新类型的同义词,它在我的所有其他代码中使用,新循环几乎相同:

{-# LANGUAGE BangPatterns #-}
module Simulation.Simulator where

import Simulation.Common

simulateUntil ::
        Int                             ->      --Number of simulation steps
        a                               ->      --Initial model
        (a -> SeachEnv Bool)              ->      --Function to check if simulation should end
        (Float -> a -> SearchEnv a)        ->      --Update model one step
        SearchEnv a 
simulateUntil !num !model !done !update = do
        !stop <- done model
        if stop then return model 
        else do updateM <- update (1 / fromIntegral num) model
                simulateUntil num updateM done update

然而出于某种原因,这个最后一版泄漏了内存,显示为

FUN_1_0

使用&#34; -h&#34; GHC中的选项。

这是&#34;类型&#34;的预期行为吗?或者还有其他事情发生了吗?

编辑: 以下是GHC&#34; -h&#34;报告的内存使用量的差异。选项: 使用类型同义词:http://i.imgur.com/X1HiUQp.png 删除类型同义词后(恢复到顶部的旧代码显示):http://i.imgur.com/FuC863z.png

2 个答案:

答案 0 :(得分:2)

两种类型的签名根本不是一回事。后者实际上是

simulateUntil ::
    ...
    -> (a -> (RandomGen g => Rand g Bool))
    -> (Float -> a -> (RandomGen g => Rand g a))
    -> (RandomGen g => Rand g a)

我无法告诉你这会导致空间泄漏的哪个方面,但一般来说这是一件非常奇怪的事情,而且这里完全不需要。

答案 1 :(得分:2)

在内部,GHC将类型类约束表示为类型类字典形式的函数参数。因此,如果您有RandomGen g => Rand g a类型,则会有效地转换为RandomGen -> Rand a。这意味着无论何时使用doneupdate的调用结果,都必须重新计算内部类型类函数。顺便说一下,这也意味着由于GHC不会自动记忆功能,因此无法共享结果。在内部,doneupdate的类型有点像这样:

done   :: a          -> (RandomGen -> Rand Bool)
update :: Float -> a -> (RandomGen -> Rand a)

我认为具体的问题是你将update的结果传递回递归调用,每次需要该值时,必须调用类型为class dictionary的内部函数。

在你的第一个版本中,RandomGen类型字典被传递给顶级函数,因此没有额外的“隐藏”函数需要被调用。

GHC通常非常善于优化这类事情,但我怀疑这是罪魁祸首。

这是一个更简单的例子。我们可以使用:set +s命令观察在GHCI中计算表达式所需的时间和内存:

λ> let fib n = if n <= 1 then 1 else fib (n-1) + fib (n-2)
λ> let { n :: Num a => a; n = fib 30 }
λ> let { m :: Int; m = fib 30 }
λ> :set +s
λ> m
1346269
(2.04 secs, 695163424 bytes)
λ> m
1346269
(0.00 secs, 1073792 bytes)
λ> 
λ> n
1346269
(2.01 secs, 669035464 bytes)
λ> n
1346269
(2.02 secs, 669032064 bytes)

这是另一个例子。这有点像fib函数,除了它在每次递归调用中添加一些常量。

λ> let { fib1 :: (Num a, Ord a, Num b) => Int -> a -> b;                fib1 m n = if n <= 1 then 1 else fromIntegral m + fib1 m (n-1) + fib1 m (n-2) }
λ> let { fib2 :: Int -> ((Num a, Ord a) => a) -> ((Num a, Ord a) => a); fib2 m n = if n <= 1 then 1 else fromIntegral m + fib2 m (n-1) + fib2 m (n-2) }
λ> :set +s
λ> fib1 1 30
2692537
(2.59 secs, 993139888 bytes)
λ> fib2 1 30
2692537
(17.98 secs, 7884453496 bytes)

由于m在第二个fib定义中被转换为函数,因此每次需要时都必须调用它,因此不会发生共享,这会导致时间和空间泄漏。