IO/Monadic 赋值运算符导致 ghci 爆炸为无限列表

IO/Monadic assign operator causing ghci to explode for infinite list

提问人:artella 提问时间:7/28/2014 最后编辑:Communityartella 更新时间:7/28/2014 访问量:361

问:

请考虑以下程序。它永远运行,没有任何用处,但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"xdotest

--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..]

为什么改变会导致ghci爆炸?print "test"x <- print "test"

p.s. 我在试图理解在 ghci 中将惰性字节串写入文件时内存爆炸时遇到了这个问题,那里的问题(我认为)基本上可以提炼到上述内容。谢谢

Haskell 内存泄漏 GHCI

评论

2赞 lpsmith 7/28/2014
对我来说,这当然似乎是 GHCi 中的一个错误。有趣的是,即使您使用优化编译模块,然后将编译后的代码加载到 GHCi 中,这也体现在 GHCi 中。如果你只是在没有 GHCi 的情况下编译并运行它,即使没有优化,它也能正常工作。因此,GHCi 中的某些内容似乎正在维护对列表头部的引用,这在模块之外甚至不可见。
2赞 Zeta 7/28/2014
对于那些真正想在 GHCi 中运行代码的人,请保持安全并使用有限的堆 () 运行 GHCi。ghci +RTS -M100m --RTS …
0赞 Reid Barton 7/29/2014
鉴于 ghc 浮动到一个顶级常量(在 Zeta 的回答中所示的核心中),编译程序和在 ghci 中运行程序之间的行为差异并不奇怪。毕竟有对 的引用和导出,实际上你可以输入 ghci,然后过一会儿按 Ctrl-C 并再次输入,它会重用 .[1..]lst_rq4mainlst_rq4mainmainmainlst_rq4

答:

10赞 Zeta 7/28/2014 #1

免责声明:我不是 GHCi 专家,也不擅长 GHC 核心。现在我已经失去了信誉,让我们试着理解会发生什么:

GHCi 和 CAF

GHCi 保留所有已评估的 CAF

通常,在加载的模块中对顶级表达式(也称为 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

突然出现了一个新的顶级表达式,即 ,它生成列表。如前所述,GHCi 保留了顶级表达式的评估,因此也会保留。lst_rq4lst_rq4

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

启用会导致在每次计算后丢弃所有顶级表达式的计算(在单个计算期间仍会保留它们)。+r

但是,由于“它们仍然在单次评估期间保留”,在这种情况下甚至对您没有帮助。不幸的是,我无法回答为什么 GHC 引入了一个新的顶级表达式。:set +r

为什么在优化的代码中会发生这种情况?

该列表仍是顶级表达式:

main2
main2 = eftInt 1 2147483647

有趣的是,GHC 实际上并没有创建一个无限列表,因为它是有界的。Int

如何摆脱泄漏?

在这种情况下,如果您将列表置于测试中,则可以将其删除:

test = do
   x <- print "test"
   rList [1..]

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

但是,我不能就此给出一般性建议。不幸的是,我的 Haskell-fu 还不够好。

评论

0赞 artella 7/28/2014
谢谢,你的帖子很有启发性。不幸的是,我无法在我的代码中使用您的解决方案!我会做一些摆弄,看看我是否能找到另一种方法。
0赞 Zeta 7/28/2014
@artella:我是这么认为的。Don 和其他答案可以为您提供更多帮助
0赞 artella 7/28/2014
谢谢。我以前看过这篇文章并尝试了那里提到的一些技巧,但不幸的是它们不起作用(例如,包装在一个函数中并传递给 )。[1..]() -> [Int]test