Haskell可以评估而不是垃圾收集列表中的随机索引吗?

时间:2014-09-09 18:13:06

标签: haskell garbage-collection lazy-evaluation

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

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

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

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

2 个答案:

答案 0 :(得分:16)

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

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

因此,在考虑评估的内容时,需要考虑两个方面。首先是 spine ,即cons单元格的结构,其次是列表包含的 values 。而不是50和99,让我们分别使用2和4。

ghci> take 2 xs
[0,1]

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

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

现在,当我们

ghci> xs !! 4
3

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

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

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

答案 1 :(得分:5)

我们问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的结果,索引100 +
  2. 的thunk   

结果的堆配置文件

分析堆上的内存有点棘手。堆分析器默认每隔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

我们不希望垃圾收集器在程序运行时收集数据,因此我们将在旋转后添加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个字节。因此,Integer生成的f中的816/16 = 51可能仍在记忆中。

我们可以通过闭包描述询问Integer来查看所有内存实际上是用于“小”-hd的内容。我们不能通过闭包描述和成本中心对内存使用进行分组,但我们可以将分析限制为使用-hc的单个成本中心,在这种情况下,我们感兴趣的是f费用中心

example +RTS -hd -hcf

这表示由f使用的所有816个字节都由S#使用,Integer的构造函数<{1}}

BEGIN_SAMPLE 0.57
S#  816
END_SAMPLE 0.57

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

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

结构和thunk的堆轮廓

这给我们留下了选项

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

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

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

很难推断出thunk的大小,但我们会试一试。指数值[50,98]将有49个风险。这些thunk中的每一个都可能持有Integer来自生成器[0..]。它还需要保持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 :个需要2400字节的内存。 49 + 1 = 50“小”Integer s S#占用了800个字节,用于49个未计算值的thunk,以及其余列表的thunk。有1568个字节可能是未计算值的49个字节,然后每个字节为32个字节或4个字。还有另外80个字节我们无法完全解释为剩下的列表留下了thunk。

记忆和时间曲线都符合我们对该计划

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