32

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

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

有这么多这些浮动,有必要考虑它们消耗了多少内存。

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

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

4

2 回答 2

31

除了 user239558 的回答之外,为了回应您的评论,我想指出一些工具,这些工具允许您检查您的值的堆表示,自己找到此类问题的答案并查看优化的效果和不同的编译方式。

ghc-数据大小

告诉你闭包的大小。在这里,您可以看到(在 64 位机器上)在评估形式和垃圾收集之后,Foo 1 2它本身需要 24 个字节,包括依赖项,总共需要 40 个字节:

Prelude GHC.DataSize 测试> 让 x = Foo 1 2
Prelude GHC.DataSize 测试> x
富 {a = 1, b = 2}
Prelude GHC.DataSize 测试> System.Mem.performGC
Prelude GHC.DataSize 测试>closureSize x
24
Prelude GHC.DataSize 测试> recursiveSize x
40

要重现这一点,您需要使用 以编译形式加载数据定义-O,否则{-# UNPACK #-}编译指示无效。

现在让我们创建一个 thunk 并看到大小显着增加:

Prelude GHC.DataSize Test> 让 thunk = 2 + 3::Int
Prelude GHC.DataSize Test> let x = Foo 1 thunk
Prelude GHC.DataSize Test> x `seq` return ()
Prelude GHC.DataSize 测试> System.Mem.performGC
Prelude GHC.DataSize 测试>closureSize x
24
Prelude GHC.DataSize 测试> 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-堆视图

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

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

我们得到(当我们向程序传递五个参数时)结果Foo (_thunk 5) 1。请注意,参数的顺序是在堆上交换的,因为指针总是在数据之前。plain5表示 thunk 的闭包存储了未装箱的参数。

作为最后一个练习,我们通过使 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会为您提供原始字节。

于 2012-12-21T09:58:13.577 回答
13

如果b被评估,它将是一个指向Int对象的指针。指针为 8 字节,Int对象由 8 字节的标头和 8 字节的标头组成Int#

所以在这种情况下,内存使用是Foo对象(8 header, 8 Int, 8 pointer)+ boxed Int(8 header, 8 Int#)。

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

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

其次,自由变量是从表达式外部获取的未计算表达式中提到的所有变量,称为闭包环境。它们是表达式的参数,需要存储在某个地方,因此它们存储在 thunk 对象中。

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

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

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

于 2012-12-21T04:38:49.940 回答