我发现了关于 GHCi 和列表的奇怪之处。
该命令需要一些时间来执行,并且只返回正确的答案。
ghci> length [1..10^8]
100000000
但是,将其绑定(bind)到变量并执行会导致 GHC 消耗大约 5 GiB 的 RAM,直到 GHCi session 结束才释放。打字
: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
将其绑定(bind)到变量然后计算它)都很快完成:
Prelude> length [1..10^8]
10000000 -- pretty fast
Prelude> let len = length [1..10^8]
Prelude> len
10000000 -- pretty fast
Prelude>
但是,使用
let
-免费语法:Prelude> len = length [1..10^8]
Prelude> len
10000000
Prelude>
需要更长的时间并分配大量内存,直到 session 结束才释放。
请注意,这是特定于 GHCi 和交互模式的——在一个真实的、编译的 Haskell 程序中,不会有问题。编译以下将快速运行并且不会消耗过多的内存:
len = length [1..10^8]
main = print len
要了解发生了什么,您应该了解 Haskell 能够对此代码执行两种潜在的优化:
当使用优化(
-O1
或 -O2
)编译此代码时,GHC 将执行优化 #2。编译后的版本将在少量的常量内存(运行时常驻几兆字节)中快速运行。如果你运行这个:$ time ./Length +RTS -s
收集统计数据,你会发现 GHC 仍然分配了大约 1.6 GB 的堆,但这实际上是为了存储单个
Integer
值,因为它们正在增加。 (由于 Haskell 中的值是不可变的,因此必须为每个增量分配一个新的 Integer
。)如果强制类型为 Int
:len = length [(1::Int)..10^8]
那么程序将只分配几千字节的堆,你可以看到确实没有分配列表。
事实证明,当这段代码在没有优化的情况下编译时(
-O0
),GHC 只执行优化#1(正如@Carl 所指出的),但它确实做得很好,以至于即使GHC 统计显示大量的堆分配,程序仍然运行得很快,内存占用非常小。但是,当这段代码在 GHCi 中编译为字节码时,不仅使用了优化 #1,而且 GHC 在垃圾收集列表方面做得并不好。生成了一个巨大的数 GB 列表,开始时垃圾收集的速度几乎与生成它的速度一样快。内存使用量最终会很大,但至少它是相对恒定的。
您可以通过打开时间/内存统计来看到这一点:
> :set +s
> length [1..10^8]
100000000
(1.54 secs, 7,200,156,128 bytes)
>
这意味着这段代码实际上分配了一个 7.2 GB 的列表;幸运的是,它几乎可以像生成它一样快地被丢弃,因此在此计算之后 GHCi 进程使用的内存仍然相当适中。
你会看到:
> let len = length [1..10^8]
> len
和:
> len = length [1..10^8]
> len
咀嚼完全相同的大量内存(约 7.2 gigs)。
不同之处在于,出于某种原因,
let
版本允许列表在计数时被垃圾收集,并且非 let
版本没有。最后,这几乎可以肯定是一个 GHCi 错误。它可能与已报告的现有空间泄漏错误之一有关(例如,Trac #12848 或 #14336),或者它可能是一个新错误。我决定将其归档为 #14789 ,所以也许有人会看看它。
关于list - 绑定(bind) `len = length xs` 然后计算 `len` 会导致 GHC 消耗大量 RAM,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48723119/