cons 单元格在 R 中存储什么?

What do cons cells store in R?

根据 R 4.1.0 文档的 Memory{base} 帮助页面,R 为“固定”和“可变”大小的对象保留了两个独立的内存区域。据我了解,可变大小的对象是用户可以在工作环境中创建的对象:向量、列表、数据框等。但是,当提到固定大小的对象时,文档相当晦涩:

[Fixed-sized objects are] allocated as an array of cons cells (Lisp programmers will know what they are, others may think of them as the building blocks of the language itself, parse trees, etc.)[.]

有人可以提供存储在 cons 单元中的固定大小对象的示例吗?为了进一步参考,我知道函数 memory.profile() 给出了 cons 单元的使用情况。例如,在我的会话中,它看起来像:

> memory.profile()
       NULL      symbol    pairlist     closure environment     promise    language 
          1       23363      623630        9875        2619       13410      200666 
    special     builtin        char     logical     integer      double     complex 
         47         696       96915       16105      107138       10930          22 
  character         ...         any        list  expression    bytecode externalptr 
     130101           2           0       50180           1       42219        3661 
    weakref         raw          S4 
       1131        1148        1132 

这些计数在数值上和概念上代表什么?例如,logical: 16105 是否引用了存储在 R 的源 code/binaries 中的 16,105 个逻辑对象(字节?,单元格?)?

我的目的是更深入地了解 R 如何在给定会话中管理内存。最后,我想我确实理解 Lisp 和 R 中的 cons cell 是什么,但是如果这个问题的答案需要首先解决这个概念,我认为开始不会有什么坏处也许从那里开始。

背景

在 C 级别,R object 只是指向称为“节点”的内存块的指针。每个节点都是一个 C struct,一个 SEXPREC 或一个 VECTOR_SEXPRECVECTOR_SEXPREC 用于 vector-like objects,包括字符串、原子向量、表达式向量和列表。 SEXPREC 适用于所有其他类型的 object。

SEXPREC 结构具有三个连续的段:

  1. 一个 header 跨越 8 个字节,指定 object 的类型和其他元数据。
  2. 指向其他节点的三个指针,在 32 位系统上(总共)跨越 12 个字节,在 64 位系统上跨越(总共)24 个字节。第一个指向 object 属性的配对列表。第二个和第三个指向垃圾收集器遍历的双向链表中的上一个和下一个节点,以释放未使用的内存。
  3. 另外三个指向其他节点的指针,同样跨越 12 或 24 个字节,尽管这些指向的内容因 object 类型而异。

VECTOR_SEXPREC 结构有上面的段 (1) 和 (2),后面是:

  1. 两个整数,在 32 位系统上跨越(总共)8 个字节,在 64 位系统上跨越 16 个字节。这些在概念上和内存中指定向量的元素数。

VECTOR_SEXPREC 结构后跟一个至少跨越 8+n*sizeof(<type>) 字节的内存块,其中 n 是相应向量的长度。该块包含一个 8 字节的前导缓冲区、向量“数据”(即向量的元素),有时还有一个尾随缓冲区。

总而言之,non-vectors 被存储为跨越 32 或 56 字节的节点,而向量被存储为跨越 28 或 36 字节的节点,后跟一个大小与数量大致成正比的数据块元素。因此节点的大小大致固定,而矢量数据需要可变的内存量。

回答

R 为称为 Ncell(或 cons 单元)的块中的节点分配内存,并为称为 Vcell 的块中的矢量数据分配内存。根据?Memory,每个Ncell在32位系统上为28字节,在64位系统上为56字节,而每个Vcell为8字节。因此,?Memory 中的这一行:

R maintains separate areas for fixed and variable sized objects.

实际上是指节点和矢量数据,而不是R objects 本身.

memory.profile 给出内存中所有 R object 使用的 cons 单元数,按 object 类型分层。因此 sum(memory.profile()) 将大致等于 gc(FALSE)[1L, "used"],这给出了垃圾 collection.

后使用的 cons 单元总数
gc(FALSE)
##          used (Mb) gc trigger (Mb) limit (Mb) max used (Mb)
## Ncells 273996 14.7     667017 35.7         NA   414424 22.2
## Vcells 549777  4.2    8388608 64.0      16384  1824002 14.0

sum(memory.profile())
## [1] 273934

当您分配新的 R object 时,gc 报告的正在使用的 Ncell 和 Vcell 数量将会增加。例如:

gc(FALSE)[, "used"]
## Ncells Vcells 
## 273933 549662

x <- Reduce(function(x, y) call("+", x, y), lapply(letters, as.name))
x
## a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + p + 
##     q + r + s + t + u + v + w + x + y + z

gc(FALSE)[, "used"]
## Ncells Vcells 
## 330337 676631

鉴于 x 是一种语言 object,而不是矢量,您可能想知道为什么使用的 Vcell 数量增加了。原因是节点是递归的:它们包含指向其他节点的指针,这些节点很可能是矢量节点。在这里,分配 Vcells 的部分原因是 x 中的每个符号都指向一个字符串(+"+"a"a",等等),每个字符串都是一个字符向量。 (也就是说,令人惊讶的是,在这种情况下需要约 125000 个 Vcell。这可能是 Reducelapply 调用的人为产物,但我目前不太确定。)

参考资料

一切都有点散乱:

  • ?Memory, ?`Memory-limits`, ?gc, ?memory.profile, ?object.size.
  • This Writing R Extensions 手册部分,了解有关 Ncell 和 Vcell 的更多信息。
  • R 内部结构手册的
  • This 部分对 R objects 的内部结构进行了完整描述。