考虑以下程序。它会永远运行并且没有任何用处,但是ghci中的内存消耗是恒定的:

--NoExplode.hs
module Main (main) where

test :: [Int] -> IO()
test lst = do
  print "test"
  rList lst

rList :: [Int] -> IO ()
rList [] = return ()
rList (x:xs) = do
  rList xs

main = do
  test [1..]

现在考虑上述的以下简单修改的​​版本。在ghci中运行该程序时,内存会爆炸。唯一的区别是现在print "test"x块中的do分配了test
--Explode.hs
module Main (main) where

test :: [Int] -> IO()
test lst = do
  x <- print "test"
  rList lst

rList :: [Int] -> IO ()
rList [] = return ()
rList (x:xs) = do
  rList xs

main = do
  test [1..]

为什么将print "test"更改为x <- print "test"会导致ghci崩溃?

ps。我在尝试了解Memory exploding upon writing a lazy bytestring to file in ghci时遇到了这个问题,那里的问题(我认为)本质上就是上述问题。谢谢

最佳答案

免责声明:我不是GHCi专家,并且对GHC内核也不是很满意。现在,我已经失去了信誉,让我们尝试了解发生了什么:

GHCi和CAF

GHCi retains all evaluated CAFs:

通常,在评估之间保留对已加载模块中顶级表达式(也称为CAF或常量应用形式)的任何评估。

现在您可能想知道为什么两个版本之间有如此大的差异。让我们看看-ddump-simpl的核心。请注意,您自己转储程序时可能要删除-dsuppress-all

您的程序转储

非爆炸版本:

❯ ghc SO.hs -ddump-simpl -fforce-recomp -O0 -dsuppress-all
[1 of 1] Compiling Main             ( SO.hs, SO.o )

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 29, types: 28, coercions: 0}

$dShow_rq2
$dShow_rq2 = $fShow[] $fShowChar

Rec {
rList_reI
rList_reI =
  \ ds_dpU ->
    case ds_dpU of _ {
      [] -> return $fMonadIO ();
      : x_aho xs_ahp -> rList_reI xs_ahp
    }
end Rec }

main
main =
  >>
    $fMonadIO
    (print $dShow_rq2 (unpackCString# "test"))
    (rList_reI (enumFrom $fEnumInt (I# 1)))

main
main = runMainIO main

重要的部分是[1..]的位置,几乎在最后:
enumFrom $fEnumInt (I# 1))

如您所见,该列表不是CAF。但是,如果我们改用爆炸版本会怎样?

爆炸版

❯ ghc SO.hs -ddump-simpl -fforce-recomp -O0 -dsuppress-all
[1 of 1] Compiling Main             ( SO.hs, SO.o )

==================== Tidy Core ====================
Result size of Tidy Core = {terms: 32, types: 31, coercions: 0}

$dShow_rq3
$dShow_rq3 = $fShow[] $fShowChar

Rec {
rList_reI
rList_reI =
  \ ds_dpV ->
    case ds_dpV of _ {
      [] -> return $fMonadIO ();
      : x_ahp xs_ahq -> rList_reI xs_ahq
    }
end Rec }

lst_rq4
lst_rq4 = enumFrom $fEnumInt (I# 1)

main
main =
  >>=
    $fMonadIO
    (print $dShow_rq3 (unpackCString# "test"))
    (\ _ -> rList_reI lst_rq4)

main
main = runMainIO main

突然有一个新的顶级表达式lst_rq4生成列表。如前所示,GHCi保留了顶级表达式的求值,因此lst_rq4也将保留。

现在有一个选项可以放弃评估:

启用+r会使所有顶级表达式的求值在每次求值后都被丢弃(在一次求值过程中仍保留它们)。

但是由于“在一次评估中它们仍然保留着”,因此即使是:set +r在这种情况下也无济于事。不幸的是,我无法回答为什么GHC引入了新的顶层表达。

为什么这甚至会在优化的代码中发生?

该列表仍然是顶级表达式:
main2
main2 = eftInt 1 2147483647

有趣的是,由于Int是有界的,GHC实际上不会创建无限列表。

一个人如何摆脱泄漏?

在这种情况下,如果将列表放在测试中,您可以摆脱它:
test = do
   x <- print "test"
   rList [1..]

这将阻止GHC创建顶级表达式。

但是,我真的不能对此提供一般性建议。不幸的是,我的Haskell-fu还不够好。

关于haskell - IO/Monadic分配运算符导致ghci爆炸导致无限列表,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/24986296/

10-13 09:28