让我们询问性能分析器。
我们将编译下面的示例程序,该程序应该大致做到与您的GHCI会话相同。重要的是,像GHCI一样,我们需要print
结果,因为这会强制执行计算。
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次,其中50次是为了
print (take 50 xs)
,一次是为了
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字节的内存。对于“小”
Integer
,一个
Integer
会消耗2个字的内存。在我的系统上,一个字是8个字节的内存,所以一个“小”
Integer
占用16个字节。因此816/16=51个
Integer
可能仍然在内存中由
f
产生。
我们可以通过使用
-hd
请求通过
闭包描述进行分组来检查所有这些内存实际上是否被用于“小”
Integer
。我们无法同时按
闭包描述和成本中心分组内存使用情况,但是我们可以使用
-hc
将分析限制为单个成本中心,在这种情况下,我们关注
f
成本中心。
example +RTS -hd -hcf
这句话意思是所有816字节归属于"f",由"small" Integer的构造函数"S#"使用。
BEGIN_SAMPLE 0.57
S
END_SAMPLE 0.57
由于保留了51个整数结果,并且它期望只保留50个整数,因此我们肯定可以删除以下内容
- 保留索引0-49的结果,对于索引50+,使用thunk(暂缓计算)
结构和thunks的堆剖面
这样我们就有了一个选择
- 保留索引0-49的结果,对于索引50-98,使用thunks,对于索引99,保留结果,对于索引100+,使用thunk
让我们猜测一下这种情况会消耗多少内存。
通常情况下,Haskell的数据类型需要1个word的内存来存储构造函数,每个字段也需要1个word。[]类型的:构造函数有两个字段,因此应该占用3个word的内存,或24字节。然后,100个:将占用2400字节的内存。当我们询问xs的闭包描述时,我们将看到这完全正确。
很难推理thunk的大小,但我们会尝试。对于索引值[50, 98]的值,将有49个thunks。这些thunk中的每一个可能都保存着来自生成器
[0..]
的
Integer
。它还需要保存thunk的结构,但遗憾的是,在进行性能分析时,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
我们的判断是完全正确的,需要2400字节的内存来存储100个“:”符号。有49+1=50个“小型”整数S#占用800字节,其中包括49个未计算值的thunk和列表剩余部分的thunk。还有1568字节,可能是未计算值的49个thunk,每个thunk为32字节或4个字。还有另外80字节,我们不能完全解释,留给列表剩余部分的thunk。
内存和时间的使用情况与我们的信念一致,程序将:
1. 保留索引0-49的结果,保留索引50-98的thunk,保留索引99的结果,保留索引100+的thunk。