thunk使用了多少内存?

时间:2012-12-21 00:57:05

标签: haskell lazy-evaluation thunk

假设我有一个非常大的数字(数百万/十亿+)这些简单的Foo数据结构:

data Foo = Foo
    { a :: {-# UNPACK #-}!Int
    , b :: Int
    }

随着这么多的浮动,有必要考虑他们消耗多少内存。

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

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

2 个答案:

答案 0 :(得分:31)

除了user239558的回答,并回应你在那里的评论,我想指出一些工具,让你检查你的价值的堆表示,自己找到这样的问题的答案,并看到效果优化和不同的编译方式。

ghc-datasize

告诉你闭包的大小。在这里,你可以看到(在一台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 #-}编译指示无效。

现在让我们创建一个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个字节的引用?要回答这个问题,我们需要一个更好的工具:

ghc-heap-view

这个库(由我)可以调查堆并准确地告诉您数据在那里的表示方式。所以将这一行添加到上面的文件中:

buildHeapTree 1000 (asBox x) >>= putStrLn . ppHeapTree

我们得到(当我们将五个参数传递给程序时)结果Foo (_thunk 5) 1。请注意,参数的顺序在堆上交换,因为指针总是在数据之前。普通5表示thunk的关闭将其参数存储为unboxed。

作为最后一个练习,我们通过在n中使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)

给出Foo (_thunk (I# 4)) 1的堆表示,其中n具有单独的闭包(由I#构造函数的存在表示),并显示值的预期大小及其总和, (24,24,16,64)

哦,如果这仍然是太高级别,getClosureRaw会给你原始字节。

答案 1 :(得分:13)

如果评估b,它将是指向Int对象的指针。指针是8个字节,Int对象包含一个8字节的头,而Int#包含8个字节。

因此,在这种情况下,内存使用量为Foo个对象(8个标头,8个Int,8个指针)+盒装Int(8个标头,8个Int#

b未评估时,Foo中的8字节指针将指向Thunk objectThunk object表示未评估的表达式。与Int对象一样,此对象具有8字节标头,但对象的其余部分由未评估表达式中的自由变量组成。

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

其次,free variables是未评估表达式中提到的所有变量,这些变量取自表达式外部,称为environment of a closure。它们是表达式的一些参数,它们需要存储在某个地方,因此它们存储在thunk对象中。

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

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

此重定向指针将指向Int对象(16个字节)。然而,现在“死”的thunk将在下一次垃圾收集时被淘汰。当GC复制Foo时,它会使Foo的b直接指向Int对象,使得thunk无法引用并因此成为垃圾。