list - 绑定(bind) `len = length xs` 然后计算 `len` 会导致 GHC 消耗大量 RAM

标签 list haskell ghc ghci

我发现了关于 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 能够对此代码执行两种潜在的优化:
  • 它可以显式地创建一个惰性列表并开始计算其长度,但确定一旦计算了列表的开头,就不再需要这些元素,从而可以立即对它们进行垃圾收集。
  • 它可以确定根本不需要创建列表,并通过称为“列表融合”的过程创建直接从 1 计数到 10^8 的编译代码,而无需尝试将这些数字放入任何类型的数据结构中。

  • 当使用优化(-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/

    相关文章:

    python - 将数据框转换为元组列表

    list - Prolog 中的 zip 函数

    haskell - Haskell 中的欧拉问题——有人能发现我的错误吗

    parallel-processing - 如何强制某组目标始终按顺序运行?

    python - 为什么 GHC 测试套件是用 Python 而不是 Haskell 编写的?

    Python 列表追加

    java - 从运行时列表中删除重复的字符串

    haskell - Haskell 是否有像 ActiveRecord 或 Sequel 这样的 SQL 查询组合库?

    haskell - 使用导出的符号

    haskell - -XStrict 在 GHC 中有什么作用吗?