让我们问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 的结果,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
- 保留索引 0 - 49 的结果,thunk 索引 50+
结构和 thunk 的堆配置文件
这给我们留下了选择
- 保留索引 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。
内存和时间配置文件都符合我们的信念,即该程序将
- 保留索引 0 - 49 的结果,索引 50 - 98 的 thunk,索引 99 的结果,索引 100+ 的 thunk