R 中 cons 单元存储什么?

Sic*_*abí 1 memory documentation r cons

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

[固定大小的对象]被分配为 cons 单元数组(Lisp 程序员会知道它们是什么,其他人可能会认为它们是语言本身的构建块、解析树等)[.]

有人可以提供一个存储在 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 
Run Code Online (Sandbox Code Playgroud)

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

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

Mik*_*gan 5

背景

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

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

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

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

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

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

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

回答

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

R 为固定和可变大小的对象维护单独的区域。

实际上指的是节点和向量数据,而不是 R 对象本身

memory.profile给出内存中所有 R 对象使用的 cons 单元数,按对象类型分层. 因此sum(memory.profile())将大致等于gc(FALSE)[1L, "used"],它给出了垃圾收集后正在使用的 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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

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

参考

一切都有点分散: