我有以下代码作为我的热循环。
{-# 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
答案 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
。这意味着无论何时使用done
或update
的调用结果,都必须重新计算内部类型类函数。顺便说一下,这也意味着由于GHC不会自动记忆功能,因此无法共享结果。在内部,done
和update
的类型有点像这样:
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
定义中被转换为函数,因此每次需要时都必须调用它,因此不会发生共享,这会导致时间和空间泄漏。