假设我有一个非常大的数字(数百万/十亿+)这些简单的Foo
数据结构:
data Foo = Foo
{ a :: {-# UNPACK #-}!Int
, b :: Int
}
随着这么多的浮动,有必要考虑他们消耗多少内存。
在64位机器上,每个Int
是8个字节,因此a
只占用8个字节(因为它是严格的并且是解包的)。但b
会占用多少内存?我想这会根据thunk是否被评估而改变,对吧?
我想在一般情况下这是不可能的,因为b
可能依赖于任何数量的内存位置,只有在需要评估b
时才会留在内存中。但是,如果b
仅依赖于a
上的某些非常昂贵的操作,该怎么办?那么,有没有确定的方法来判断将使用多少内存?
答案 0 :(得分: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 #-}
编译指示无效。
现在让我们创建一个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
。请注意,参数的顺序在堆上交换,因为指针总是在数据之前。普通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 object。 Thunk 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无法引用并因此成为垃圾。