【问题标题】:IO/Monadic assign operator causing ghci to explode for infinite listIO/Monadic 分配运算符导致 ghci 为无限列表爆炸
【发布时间】:2014-07-27 22:32:05
【问题描述】:

考虑以下程序。它永远运行,没有任何用处,但 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" 现在分配给testdo 块中的x

--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 &lt;- print "test" 会导致 ghci 崩溃?

附言我在尝试理解Memory exploding upon writing a lazy bytestring to file in ghci 时遇到了这个问题,而那里的问题(我认为)基本上归结为上述问题。谢谢

【问题讨论】:

  • 在我看来,这显然是 GHCi 中的一个错误。有趣的是,即使您使用优化编译模块然后将编译后的代码加载到 GHCi 中,这也会体现在 GHCi 中。如果您只编译它并在没有 GHCi 的情况下运行它,即使没有优化,它也可以正常工作。因此,GHCi 中的某些东西似乎在维护对列表头部的引用,这在模块之外甚至不可见。
  • 对于那些真正想在 GHCi 中运行代码的人,请注意安全并以有限的堆 (ghci +RTS -M100m --RTS …) 运行 GHCi。
  • 鉴于 ghc 将 [1..] 浮动到顶级常量(Zeta 的答案中显示的核心中的 lst_rq4),编译程序和在 ghci 中运行程序之间的行为差​​异并不奇怪.毕竟main 引用了lst_rq4 并且导出了main,确实可以在ghci 中输入main,然后按Ctrl-C 并再次输入main,它会重用lst_rq4的计算。

标签: haskell memory-leaks ghci


【解决方案1】:

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

GHCi 和 CAF

GHCiretains 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

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

如何消除泄漏?

在这种情况下,如果您将列表 放入 测试中,您可以摆脱它:

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

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

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

【讨论】:

  • 谢谢,你的帖子很有启发性。不幸的是,我无法在我的代码中使用您的解决方案!我会做一些摆弄,看看我是否能找到另一种方法。
  • 谢谢。我之前看过这篇文章并尝试了其中提到的一些技巧,但不幸的是它们不起作用(例如,将[1..] 包装在() -&gt; [Int] 函数中并传递给test)。
猜你喜欢
  • 2023-03-26
  • 1970-01-01
  • 2012-06-13
  • 1970-01-01
  • 1970-01-01
  • 2015-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多