List 绑定'len=length xs',然后计算'len',会导致GHC消耗大量RAM

List 绑定'len=length xs',然后计算'len',会导致GHC消耗大量RAM,list,haskell,ghc,ghci,List,Haskell,Ghc,Ghci,我发现了一件关于GHCi和列表的奇怪事情 这个命令需要一些时间来执行,并且只返回正确的答案 ghci> length [1..10^8] 100000000 然而,将其绑定到一个变量并执行会导致GHC消耗大约5GiB的RAM,而不会释放,直到GHCi会话结束。键入:在实际退出前消耗3 GiB后退出 ghci> len = length [1..10^8] ghci> len -- Consumes 5 GiB 100000000 ghci> :quit -- Consu

我发现了一件关于GHCi和列表的奇怪事情

这个命令需要一些时间来执行,并且只返回正确的答案

ghci> length [1..10^8]
100000000
然而,将其绑定到一个变量并执行会导致GHC消耗大约5GiB的RAM,而不会释放,直到GHCi会话结束。键入<代码>:在实际退出前消耗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中重现这一点。直接计算表达式(或使用
将其绑定到变量,然后对其进行计算)都可以快速完成:

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。编译后的版本将在少量恒定内存中快速运行(运行时驻留的内存为数兆字节)。如果使用以下选项运行此操作:

    $ time ./Length +RTS -s
    
    要收集统计数据,您会发现GHC仍在分配大约1.6GB的堆,但实际上这是在单个
    整数
    值递增时存储这些值。(由于Haskell中的值是不可变的,因此必须为每个增量分配一个新的
    整数)

    len = length [(1::Int)..10^8]
    
    然后程序将只分配几千字节的堆,您可以看到实际上没有分配任何列表

    事实证明,当编译这段代码时没有进行优化(
    -O0
    ),GHC只执行优化1(正如@Carl所指出的),但它确实做得很好,以至于即使GHC统计数据显示了大量堆分配,该程序仍然以非常小的内存占用运行得非常快

    然而,当这段代码在GHCi中编译成字节码时,不仅使用了优化#1,而且GHC在垃圾收集列表方面也做得不太好。生成了一个巨大的数千字节的列表,开始时垃圾收集的速度几乎与生成的速度一样快。内存使用量最终相当大,但至少相对稳定

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

    > :set +s
    > length [1..10^8]
    100000000
    (1.54 secs, 7,200,156,128 bytes)
    >
    
    这意味着该代码实际上分配了一个7.2G的列表;幸运的是,它几乎可以以生成的速度丢弃,因此GHCi进程在计算之后使用的内存仍然相当有限

    您将看到:

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

    仔细咀嚼同样巨大的内存(大约7.2千兆)

    不同之处在于,出于某种原因,
    let
    版本允许对列表进行垃圾收集,而非
    let
    版本则不允许


    最后,这几乎肯定是一个GHCi错误。它可能与已报告的某个现有空间泄漏漏洞有关(例如,Trac或),也可能是一个新的漏洞。我决定将其归档为,这样也许有人会看一看。

    我可以复制它。Debian Jessie x64,堆栈lts-9.21。在我看来,这似乎是某种运行时泄漏。请注意,gc不报告大内存驻留。如果使用“let len=length[1..10^8]”,则不会发生泄漏。可能与没有let的赋值是ghci的一个新特性有关。@max630哦,有趣的是,我甚至没有考虑过这一点,在所有测试中都编写了
    let
    。如果没有
    let
    ,这确实会耗尽GHC-8.2.1和8.3中的内存。缺陷还是只是模糊了评估策略中的差异?这将是非常奇怪的。使用GHC 8.2.2,我可以重现泄漏,并且可以通过
    let
    确认它不存在。这可能是不相关的,但我看到省略
    let
    和使用
    :show bindings
    会显示另外一个没有的绑定,也就是说,
    $trModule::GHC.Types.Module=.
    .Ghci第7版甚至不允许我这样做,即在不使用
    let
    关键字的情况下声明变量。我很确定列表融合不会在
    -O0
    发生。我似乎记得它是根据重写规则实现的,这些规则要求
    -O1
    或更高。嗯,你说得对。我被甩了,因为
    -O0
    版本仍然在小而恒定的内存中运行。我试图更新答案。(1)不是优化,而是语言的工作方式。我想知道为什么它不将
    length[a..b]
    优化为减法和增量。。。。还没有实施吗?
    > len = length [1..10^8]
    > len