Haskell数据类型的内存占用

128

在Haskell中(主要是使用GHC),我如何找到存储某种数据类型的实际内存量?是否可以在运行时(例如在GHCi中)计算它,或者可以从组件估计复合数据类型的内存需求?

通常情况下,如果已知类型a和b的内存需求,那么代数数据类型的内存开销是多少,例如:

data Uno = Uno a
data Due = Due a b
例如,这些值在内存中占用多少字节?
1 :: Int8
1 :: Integer
2^100 :: Integer
\x -> x + 1
(1 :: Int8, 2 :: Int8)
[1] :: [Int8]
Just (1 :: Int8)
Nothing

我知道实际内存分配会更高,因为垃圾回收是延迟的。由于惰性求值,实际内存分配可能会有显著不同(而thunk的大小与值的大小无关)。问题是,对于一个数据类型,在完全求值时其值需要多少内存?

我发现在GHCi中有一个:set +s选项来查看内存统计信息,但不清楚如何估算单个值的内存占用。

2个回答

159

(以下内容适用于GHC,其他编译器可能使用不同的存储约定)

经验法则: 构造函数的头部占用一个字(Word),每个字段也占用一个字。例外情况: 没有字段的构造函数(例如NothingTrue)不占空间,因为GHC会创建这些构造函数的单个实例并在所有用途之间共享。

在32位计算机上,一个字是4个字节,在64位计算机上,一个字是8个字节。

例如:

data Uno = Uno a
data Due = Due a b

一个 Uno 需要2个单词,而一个 Due 需要3个单词。

Int 类型的定义如下:

data Int = I# Int#
现在,Int#占用一个字,因此Int总共占用2个字。大多数未装箱的类型只需要一个字,例外情况是Int64#Word64#Double#(在32位机器上)需要占用2个字。 GHC实际上具有类型为IntChar的小值缓存,因此在许多情况下它们根本不需要堆空间。 除非您使用Char > 255,否则String仅需要为列表单元格分配空间。 Int8Int具有相同的表示方式。 Integer的定义如下:
data Integer
  = S# Int#                            -- small integers
  | J# Int# ByteArray#                 -- large integers

一个小的 IntegerS#)占用2个字,但是一个大的整数根据其值需要占用可变数量的空间。一个 ByteArray# 占用2个字(头信息 + 大小)和数组本身所需的空间。

请注意,使用 newtype 定义的构造函数是免费的。 newtype 只是一种编译时的概念,在运行时不占用任何空间或指令。

更多细节请见 GHC Commentary 中 Heap Objects 的布局


2
标题不是两个单词吗?一个用于标记,另一个用于在GC或评估期间使用的转发指针?那么这不会使您的总字数增加一个单词吗? - Edward Kmett
6
@Edward: Thunks被间接引用(后来被GC移除)所覆盖,但这只有两个单词,并且每个堆对象至少保证为2个单词大小。如果没有打开任何分析或调试功能,则头文件确实只有一个单词。在GHC中是这样,其他实现可能会有所不同。 - nominolo
3
nominolo: 是的,但来自Closure.h:/* Thunk(转跳函数)具有填充字用于存储更新后的值。这是为了避免更新覆盖有效载荷,从而在进入和更新过程中避免需要锁定Thunk。注意:这不适用于THUNK_STATIC,它们没有有效载荷。注意:我们在所有情况下都保留此填充字,而不仅仅是在SMP(对称多处理)情况下,这样我们就不必为SMP重新编译所有库。*/在一个间接寻址中,有效载荷不会被覆盖。该间接寻址将被写入Header的另一个位置。 - Edward Kmett
6
是的,但请注意这仅适用于“thunks”。它不适用于构造函数。估计一个thunk的大小有点困难——你需要计算自由变量的数量。 - nominolo
1
你真的会为单构造函数数据类型获得标题开销吗?这不能被优化掉吗? - Lii
显示剩余10条评论

5

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接