【问题标题】:Can Haskell evaluate and not garbage collect random indexes in a list?Haskell 可以评估而不是垃圾收集列表中的随机索引吗?
【发布时间】:2014-11-03 06:18:17
【问题描述】:

据我了解,Haskell 仅在某些内容超出范围时进行垃圾收集,因此顶级绑定只会被评估一次并且永远不会超出范围。因此,如果我在 GHCI 中运行此代码,前 50 个元素将被评估并保存。

let xs = map f [0..]
take 50 xs

我的问题是当我执行以下 sn-p 时会发生什么:xs !! 99。垃圾收集器保存了什么?有吗

  1. 保留索引 0 - 49 的结果,索引 50 - 98 的 thunk,索引 99 的结果,索引 100+ 的 thunk
  2. 保留索引 0 - 49 的结果,thunk 索引 50+
  3. 保留索引 0 - 99 的结果,索引 100+ 的结果

【问题讨论】:

  • 使用索引,而不是索引。
  • mmap-bytestring 包可以让你 GC 一个连续数组的中间片段

标签: haskell garbage-collection lazy-evaluation


【解决方案1】:

Haskell 列表是由(:) ("cons") 单元组成并以[] ("nil") 值终止的链表。我会画这样的单元格

 [x] -> (tail - remainder of list)
  |
  v
(head - a value)

因此,在考虑评估什么时,需要考虑两个方面。第一个是 spine,即 cons 单元的结构,第二个是列表包含的 values。我们分别用 2 和 4 代替 50 和 99。

ghci> take 2 xs
[0,1]

打印此列表强制评估前两个 cons 单元格以及其中的值。所以你的列表看起来像这样:

[x] -> [x] -> (thunk)
 |      |
 v      v
 0      1

现在,当我们

ghci> xs !! 4
3

我们没有要求第二个或第三个值,但我们需要评估这些 cons 单元格以到达第 4 个元素。所以我们强制脊柱一直到第 4 个元素,但我们只计算了第 4 个值,所以列表现在看起来像这样:

[x] -> [x] -> [x] -> [x] -> [x] -> (thunk)
 |      |      |      |      |
 v      v      v      v      v
 0      1   (thunk) (thunk)  4

这张图片中的任何内容都不会被垃圾收集。但是,有时这些 thunk 可能会占用大量空间或引用较大的内容,并且将它们评估为普通值将允许释放这些资源。请参阅this answer,了解这些细微之处的小讨论。

【讨论】:

  • 谢谢!我不知道 (:) 和值是分开评估的。请问你这是从哪里学来的?有没有关于 Haskell GC 工作原理的好文章?
  • @RamithJayatilleka 知道这些是分开的来自理解按需调用,我主要通过在了解基本规则后手动减少 lambda 表达式来做到这一点。 GC 是这种缩减策略的一个实现细节,负责释放用于表示术语的内存。
【解决方案2】:

让我们问the profiler

我们将编译以下示例程序,该程序应该与您的 GHCI 会话大致相同。我们print 的结果很重要,就像 GHCI 所做的那样,因为这会强制计算。

f x = (-x)

xs = map f [0..]

main = do
    print (take 50 xs)
    print (xs !! 99)

我将我的保存为example.hs。我们将使用启用分析的选项对其进行编译

ghc -prof -fprof-auto -rtsopts example.hs

时间概况

我们可以找出有多少次f 被应用到时间配置文件中。

profile +RTS -p

这会产生一个名为example.prof 的输出文件,以下是有趣的部分:

COST CENTRE MODULE                     no.     entries
...
   f        Main                        78          51 

我们可以看到f 被评估了 51 次,print (take 50 xs) 被评估了 50 次,print (xs !! 99) 被评估了一次。因此我们可以排除您的第三种可能性,因为 f 只被评估了 51 次,所以不可能有所有索引 0-99 的结果

  1. 保留索引 0 - 99 的结果,thunk 索引 100+

结果的堆配置文件

分析堆上的内存有点棘手。默认情况下,堆分析器每 0.1 秒采样一次。我们的程序将运行得如此之快,以至于堆分析器在运行时不会采集任何样本。我们将在我们的程序中添加一个旋转,以便堆分析器有机会进行采样。以下将旋转几秒钟。

import Data.Time.Clock

spin :: Real a => a -> IO ()
spin seconds =
    do
        startTime <- getCurrentTime 
        let endTime = addUTCTime (fromRational (toRational seconds)) startTime
        let go = do
            now <- getCurrentTime
            if now < endTime then go else return ()
        go

我们不希望垃圾收集器在程序运行时收集数据,因此我们将在 spin 之后添加 xs 的另一个用法。

main = do
    print (take 50 xs)
    print (xs !! 99)
    spin 1
    print (xs !! 0)

我们将使用默认的堆分析选项运行它,该选项按成本中心对内存使用情况进行分组。

example +RTS -h

这会生成文件example.hp。我们将从文件中间的数字稳定的样本中提取一个样本(当它在 spin 中时)。

BEGIN_SAMPLE 0.57
(42)PINNED  32720
(48)Data.Time.Clock.POSIX.CAF   48
(85)spin.endTime/spin/mai...    56
(84)spin.go/spin/main/Mai...    64
(81)xs/Main.CAF 4848
(82)f/xs/Main.CAF   816
(80)main/Main.CAF   160
(64)GHC.IO.Encoding.CAF 72
(68)GHC.IO.Encoding.CodeP...    136
(57)GHC.IO.Handle.FD.CAF    712
(47)Main.CAF    96
END_SAMPLE 0.57

我们可以看到f 产生了 816 字节的内存。对于"small" Integers, an Integer consumes 2 word of memory。在我的系统上,一个词是 8 个字节的内存,所以一个“小”Integer 需要 16 个字节。因此,f 产生的Integers 中有 816/16 = 51 个可能仍在内存中。

我们可以通过闭包描述-hd请求配置文件来检查所有这些内存实际上是否被用于“小”Integers。我们不能同时通过 closure 描述 和成本中心对内存使用进行分组,但我们可以使用 -hc 将分析限制在单个成本中心,在这种情况下,我们对 @ 感兴趣987654350@成本中心

example +RTS -hd -hcf

这表示由于f 而产生的所有816 字节都被S# 使用,“small”的构造函数Integers

BEGIN_SAMPLE 0.57
S#  816
END_SAMPLE 0.57

我们当然可以删除以下内容,因为保留了 51 个Integer 结果,并且预计只保留 50 个Integers

  1. 保留索引 0 - 49 的结果,thunk 索引 50+

结构和 thunk 的堆配置文件

这给我们留下了选择

  1. 保留索引 0 - 49 的结果,索引 50 - 98 的 thunk,索引 99 的结果,索引 100+ 的 thunk

让我们猜测一下这种情况会消耗多少内存。

一般来说,Haskell data 类型需要 1 个字的构造函数和 1 个字的每个字段。 [] 类型的 : 构造函数有两个字段,因此它应该占用 3 个字的内存,或 24 个字节。 100 :s 将占用 2400 字节的内存。当我们询问xs闭包描述时,我们会发现这是完全正确的。

很难推断 thunk 的大小,但我们会试一试。索引 [50, 98] 的值将有 49 个 thunk。这些 thunk 中的每一个都可能持有来自生成器 [0..]Integer。它还需要保存一个 thunk 的结构,不幸的是,它在分析时会发生变化。列表的其余部分也会有一个 thunk。它需要 Integer 来生成列表的其余部分,以及 thunk 的结构。

根据xs 成本中心的闭包描述要求内存细分

example +RTS -hd -hcxs

为我们提供以下示例

BEGIN_SAMPLE 0.60
<base:GHC.Enum.sat_s34b>    32
<base:GHC.Base.sat_s1be>    32
<main:Main.sat_s1w0>    16
S#  800
<base:GHC.Base.sat_s1bd>    1568
:   2400
END_SAMPLE 0.60

我们完全正确地认为有 100 个:s 需要 2400 字节的内存。有 49+1 = 50 个“小”Integers S# 占用 800 个字节用于 49 个未计算值的 thunk 和剩余列表的 thunk。有 1568 个字节,这可能是未计算值的 49 个 thunk,每个都是 32 个字节或 4 个字。还有另外 80 个字节我们无法准确解释,这些字节留给列表其余部分的 thunk。

内存和时间配置文件都符合我们的信念,即该程序将

  1. 保留索引 0 - 49 的结果,索引 50 - 98 的 thunk,索引 99 的结果,索引 100+ 的 thunk

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-04-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-14
    • 2013-08-10
    • 1970-01-01
    • 2023-04-05
    相关资源
    最近更新 更多