Mik*_*cki 32 haskell lazy-evaluation thunk
假设我有一个非常大的数字(数百万/十亿+)这些简单的Foo数据结构:
data Foo = Foo
    { a :: {-# UNPACK #-}!Int
    , b :: Int
    }
随着这么多的浮动,有必要考虑他们消耗多少内存.
在64位机器上,每个Int都是8个字节,因此a只需要8个字节(因为它是严格的和解压缩的).但是会b占用多少内存?我想这会根据thunk是否被评估而改变,对吧?
我想在一般情况下这是不可能的,因为b可能依赖于任何数量的内存位置,只有在b需要评估的情况下才会留在内存中.但是,如果b只依赖(一些非常昂贵的操作)a呢?那么,是否有一种确定性的方式来判断将使用多少内存?
Joa*_*ner 31
除了user239558的回答,并回应你在那里的评论,我想指出一些工具,让你检查你的价值的堆表示,自己找到这样的问题的答案,并看到优化的效果和不同的编译方式.
告诉你闭包的大小.在这里,您可以看到(在64位计算机上)评估形式和垃圾收集后,Foo 1 2它自己需要24个字节,包括依赖项,总共40个字节:
Prelude GHC.DataSize Test> let x = Foo 1 2
Prelude GHC.DataSize Test> x
Foo {a = 1, b = 2}
Prelude GHC.DataSize Test> System.Mem.performGC
Prelude GHC.DataSize Test> closureSize x
24
Prelude GHC.DataSize Test> recursiveSize x
40
要重现这一点,您需要以编译形式加载数据定义-O,否则,{-# UNPACK #-}pragma无效.
现在让我们创建一个thunk并看到大小明显增加:
Prelude GHC.DataSize Test> let thunk = 2 + 3::Int Prelude GHC.DataSize Test> let x = Foo 1 thunk Prelude GHC.DataSize Test> x `seq` return () Prelude GHC.DataSize Test> System.Mem.performGC Prelude GHC.DataSize Test> closureSize x 24 Prelude GHC.DataSize Test> recursiveSize x 400
现在这太过分了.原因是该计算包括对静态闭包,Num类型类词典等的引用,并且通常GHCi字节码非常不优化.所以让我们把它放在一个适当的Haskell程序中.运行
main = do
    l <- getArgs
    let n = length l
    n `seq` return ()
    let thunk = trace "I am evaluated" $ n + n
    let x = Foo 1 thunk
    a x `seq` return ()
    performGC
    s1 <- closureSize x
    s2 <- closureSize thunk
    r <- recursiveSize x
    print (s1, s2, r)
给出(24, 24, 48),所以现在Foo价值由Foo自己和一个thunk组成.为什么只有thunk,不应该还有一个引用n添加另外16个字节?要回答这个问题,我们需要一个更好的工具:
这个库(由我)可以调查堆并准确地告诉您数据在那里的表示方式.所以将此行添加到上面的文件中:
buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree
我们得到(当我们将五个参数传递给程序时)结果Foo (_thunk 5) 1.请注意,参数的顺序在堆上交换,因为指针总是在数据之前.plain 5指出thunk的关闭将其参数保存为unboxed.
作为最后一个练习,我们通过使thunk懒惰来验证这一点n:现在
main = do
    l <- getArgs
    let n = length l
    n `seq` return ()
    let thunk = trace "I am evaluated" $ n
    let x = Foo 1 thunk
    a x `seq` return ()
    performGC
    s1 <- closureSize x
    s2 <- closureSize thunk
    s3 <- closureSize n
    r <- recursiveSize x
    buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree
    print (s1, s2, s3, r)
给出一个Foo (_thunk (I# 4)) 1带有单独闭包的堆表示n(如I#构造函数的存在所示),并显示值及其总数的预期大小(24,24,16,64).
哦,如果这仍然是太高级别,getClosureRaw会给你原始字节.
use*_*558 13
如果b被计算,它将是一个指向Int对象的指针.指针是8个字节,Int对象由8个字节的头部组成,其中8个Int#字节.
因此,在这种情况下,内存使用是Foo对象(8头Int,8,8指针)+盒装Int(8头,8 Int#).
如果b未评估,则8字节指针Foo将指向Thunk对象.所述咚对象表示一个未计算的表达式.与Int对象一样,此对象具有8字节标头,但对象的其余部分由未评估表达式中的自由变量组成.
首先,此thunk对象中保存的自由变量的数量取决于创建Foo对象的表达式.创建Foo的不同方法将创建可能不同大小的thunk对象.
其次,自由变量是未评估表达式中提到的从表达式外部获取的所有变量,称为闭包的环境.它们是表达式的一些参数,它们需要存储在某个地方,因此它们存储在thunk对象中.
因此,您可以查看调用Foo构造函数的实际位置,并查看第二个参数中的自由变量数,以估计thunk的大小.
Thunk对象与大多数其他编程语言中的闭包实际上相同,但有一个重要区别.在评估时,它可以被指向评估对象的重定向指针覆盖.因此它是一个自动记忆其结果的闭包.
此重定向指针将指向Int对象(16个字节).然而,现在"死"的thunk将在下一次垃圾收集时被淘汰.当GC复制Foo时,它会使Foo的b直接指向Int对象,使thunk无法引用并因此变为垃圾.