我发现了关于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到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,所以也许有人会看看它。