除了 user239558 的回答之外,为了回应您的评论,我想指出一些工具,这些工具允许您检查您的值的堆表示,自己找到此类问题的答案并查看优化的效果和不同的编译方式。
告诉你闭包的大小。在这里,您可以看到(在 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 个字节的引用?为了回答这个问题,我们需要一个更好的工具:
这个库(我的)可以调查堆并准确地告诉你你的数据是如何在那里表示的。所以将这一行添加到上面的文件中:
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会为您提供原始字节。