据我了解,Haskell只有当某些内容超出范围时才会收集垃圾,因此顶级绑定只会被评估一次并且永远不会超出范围。因此,如果我在GHCI中运行此代码,将评估并保存前50个元素。
let xs = map f [0..]
take 50 xs
我的问题是当我执行以下代码段时会发生什么:xs !! 99
。垃圾收集器保存了什么?是吗
答案 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
- 保留索引0 - 99的结果,索引100 +
的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" Integer
s, 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
个
- 保留索引0 - 49的结果,索引50 +
的thunk 醇>
这给我们留下了选项
- 保留索引0 - 49的结果,索引50 - 98的thunk,索引99的结果,索引的thunk 100 +
醇>
让我们猜猜这种情况会消耗多少内存。
通常,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。
记忆和时间曲线都符合我们对该计划
的看法
- 保留索引0 - 49的结果,索引50 - 98的thunk,索引99的结果,索引的thunk 100 +
醇>