Haskell thunk使用多少内存?

Haskell thunk使用多少内存?,haskell,lazy-evaluation,thunk,Haskell,Lazy Evaluation,Thunk,假设我有大量(数百万/数十亿)这样简单的Foo数据结构: data Foo = Foo { a :: {-# UNPACK #-}!Int , b :: Int } 有了这么多的内存,就有必要考虑它们消耗了多少内存 在64位机器上,每个Int是8个字节,因此a只需要8个字节(因为它是严格且未打包的)。但是b会占用多少内存?我想这会随着thunk是否被评估而改变,对吗 我想在一般情况下,这是不可能的,因为b可能依赖于任何数量的内存位置,这些位置仅在b需要评估的情况下保留在

假设我有大量(数百万/数十亿)这样简单的
Foo
数据结构:

data Foo = Foo
    { a :: {-# UNPACK #-}!Int
    , b :: Int
    }
有了这么多的内存,就有必要考虑它们消耗了多少内存

在64位机器上,每个
Int
是8个字节,因此
a
只需要8个字节(因为它是严格且未打包的)。但是
b
会占用多少内存?我想这会随着thunk是否被评估而改变,对吗


我想在一般情况下,这是不可能的,因为
b
可能依赖于任何数量的内存位置,这些位置仅在
b
需要评估的情况下保留在内存中。但是如果
b
仅仅依赖于(一些非常昂贵的操作)
a
,该怎么办呢?那么,是否有一种确定的方法来判断将使用多少内存?

如果计算
b
,它将是指向
Int
对象的指针。指针是8个字节,
Int
对象由一个8字节的头和一个8字节的
Int
组成

因此在这种情况下,内存使用是
Foo
对象(8头,8
Int
,8指针)+装箱
Int
(8头,8
Int

b
未计算时,
Foo
中的8字节指针将指向a。表示未计算的表达式。与
Int
对象类似,该对象有一个8字节的头,但该对象的其余部分由未赋值表达式中的自由变量组成

所以首先,这个thunk对象中的自由变量的数量取决于创建Foo对象的表达式。创建Foo的不同方法将创建可能大小不同的thunk对象

其次,是未赋值表达式中提到的所有变量,这些变量取自表达式外部,称为。它们是表达式的某种参数,需要存储在某个地方,所以它们存储在thunk对象中

因此,您可以查看调用Foo构造函数的实际位置,并查看第二个参数中的自由变量数量,以估计thunk的大小

Thunk对象实际上与大多数其他编程语言中的闭包相同,但有一个重要区别。当对其求值时,它可以被指向求值对象的重定向指针覆盖。因此,它是一个自动记忆其结果的闭包


此重定向指针将指向
Int
对象(16字节)。但是,现在“死”的thunk将在下一次垃圾收集时被消除。当GC复制Foo时,它将使Foo的b直接指向Int对象,从而使thunk不被引用,从而成为垃圾。

除了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 现在这太过分了。原因是此计算包括对静态闭包、
Num
typeclass字典等的引用,并且通常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)
我们得到(当我们将五个参数传递给程序时)结果
Foo(\u thunk 5)1
。注意,参数的顺序在堆上交换,因为指针总是在数据之前。普通的
5
表示thunk的闭包存储其未绑定的参数

作为最后一个练习,我们通过在
n
:Now中使thunk变懒来验证这一点

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)
main=do

简而言之,你不想让任何人再次指向真空开罗,但你想解释一下thunks在内存中的表示方式?@ThomasM.DuBuisson Correct。有一种确定性的方法来确定一些代码将使用多少内存。“但你不会喜欢它的。”丹尼尔瓦格纳经验主义地测量它?你说得对。这方面的主要问题是,所有的
b
都可能有不同的大小,我想给客户一些关于预期的指导。如果能够对这些大小设置一些(理论上的)界限,那就太好了。我在哪里可以找到关于这些内部数据结构的更多信息?谢谢,关于引擎盖下发生的事情的非常有趣的信息。我支持Mike Izbicki的问题:您是否有提供更多这些实现细节的链接? buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree
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)