绑定`len =长度xs`,然后计算`len`''''''''ghc消耗了很多RAM



我发现了关于GHCI和列表的奇怪的事情。

此命令需要一些时间来执行并返回正确的答案。

ghci> length [1..10^8]
100000000

但是,将其绑定到变量并执行,导致GHC消耗了大约5 GIB RAM,而无需释放,直到GHCI会话结束为止。键入:quit在实际退出之前将其消耗3个GIB后。

ghci> len = length [1..10^8]
ghci> len
-- Consumes 5 GiB
100000000
ghci> :quit
-- Consumes 3 GiB
-- Exits

这是正常的吗?命令有什么区别?

GHC版本是8.2.2。

更新: -O0执行的优化与我首先理解的略有不同。另外,添加了有关提交新的trac错误的注释。

我可以在GHC 8.2.2中复制它。直接评估表达式(或使用let将其绑定到变量,然后对其进行评估(既快速完成:

>
Prelude> length [1..10^8]
10000000    -- pretty fast
Prelude> let len = length [1..10^8]
Prelude> len
10000000    -- pretty fast
Prelude>

但是,使用 let -free语法:

Prelude> len = length [1..10^8]
Prelude> len
10000000
Prelude>

花费更长的时间,并分配了大量内存,直到会话结束才释放。

请注意,这是针对GHCI和交互式模式的 - 在一个真实的,编译的Haskell程序中,没有问题。编译以下将迅速运行,而不会消耗多余的内存:

len = length [1..10^8]
main = print len

要了解发生了什么,您应该了解Haskell能够对此代码进行两个潜在的优化:

  1. 它可以明确创建一个懒惰的列表并开始计算其长度,但确定一旦计数列表的开始,这些元素就不需要再次允许它们立即收集垃圾。
  2. 它可以确定根本不需要创建列表,并且通过称为"列表融合"的过程,创建编译的代码,该代码直接计数从1到10^8,而无需尝试将这些数字放入任何形式数据结构。

使用优化(-O1-O2(编译此代码时,GHC将执行优化#2。编译的版本将以少量的恒定内存快速运行(运行时居民的几个Megabytes居民(。如果您使用以下操作:

$ time ./Length +RTS -s

要收集统计信息,您会发现GHC仍在分配约1.6千兆字节的堆,但这实际上是为了存储各个Integer值,它们在增加时。(由于Haskell中的值是不变的,因此必须为每个增量分配一个新的Integer。(如果您强迫该类型为Int

len = length [(1::Int)..10^8]

那么该程序将仅分配几千堆堆,您可以看到确实没有分配列表。

事实证明,当无优化(-O0(编译此代码时,GHC仅执行优化#1(@CARL指出(,但是它设法做得非常好,以至于甚至尽管GHC统计数据显示了很多堆的分配,但程序 stly 的运行速度很快,记忆足迹很小。

但是,当将此代码汇编为GHCI中的字节代码时,不仅是使用优化#1,而且GHC在收集列表的垃圾收集方面做得很好。生成了一个巨大的多gabyte列表,开头是收集的 的生成速度。记忆使用最终很大,但至少它相对恒定。

您可以通过打开计时/内存统计信息来看到这一点:

> :set +s
> length [1..10^8]
100000000
(1.54 secs, 7,200,156,128 bytes)
>

这意味着此代码实际上分配了7.2千兆字节列表;幸运的是,它几乎可以与生成一样快地扔掉,因此该计算后GHCI进程使用的内存仍然是合理的。

您会看到:

> let len = length [1..10^8]
> len

和:

> len = length [1..10^8]
> len

咀嚼完全相同的大量内存(大约7.2 gigs(。

区别在于,出于某种原因,let版本允许按计数计算列表,而非let版本则不会。

最后,这几乎可以肯定是一个GHCI错误。它可能与已报道的现有太空透明错误之一有关(例如TRAC#12848或#14336(,或者也许是新的。我决定将其提交为#14789,所以也许有人会看看它。

相关内容

  • 没有找到相关文章

最新更新