tracemem 函数(需要编译 R 以支持它)提供了复制发生时间的指示。这就是你所做的
> a <- 1:1000000; tracemem(a)
[1] "<0x7f791b39e010>"
> a[1] = 2
tracemem[0x7f791b39e010 -> 0x7f791a9d4010]:
确实有一个副本。但这是因为您将 a 从一个整数向量(1:1000000 创建一个整数序列)强制转换为一个数字向量(因为 2 是一个数值,而 R 强制转换为一个通用类型)。相反,如果您使用整数值更新整数向量,或者使用数值更新数字向量,则不会复制
> a <- 1:1000000; tracemem(a)
[1] "<0x7f791a4ef010>"
> a[1] = 2L
> a = c(1, 2, 3); tracemem(a)
[1] "<0x5180470>"
> a[1] = 2
>
更深入的了解来自对 R 的内存管理工作原理的肤浅理解。每个分配都有一个与之关联的 NAMED 级别。 NAMED=0 或 1 表示最多有 1 个符号引用它;因此,原地复制是安全的。 NAMED=2 表示存在或已经存在至少 2 个符号指向同一位置,并且任何更新值的尝试都需要复制以保持 R 的“更改时复制”的错觉。下面揭示了a 的一些内部结构,包括它的类型为 INTSXP(整数)和 NAM(1)(命名级别 1),并且它正在被跟踪。因此更新(使用整数!)不需要副本。
> a = 1:10; tracemem(a); .Internal(inspect(a))
[1] "<0x5170818>"
@5170818 13 INTSXP g0c4 [NAM(1),TR] (len=10, tl=0) 1,2,3,4,5,...
> a[1] = 2L
>
另一方面,这里有两个符号指的是内存中的位置,因此 NAMED 是 2 并且需要一个副本
> a = b = 1:10; tracemem(a); .Internal(inspect(a))
[1] "<0x576d1a0>"
@576d1a0 13 INTSXP g0c4 [NAM(2),TR] (len=10, tl=0) 1,2,3,4,5,...
> a[1] = 2L
tracemem[0x576d1a0 -> 0x576d148]:
NAMED 很难推理,所以在某种程度上,这些类型的游戏对它们来说是徒劳的。
inspect 返回其他信息。每个 R 类型在内部都表示为“SEXP”(S 表达式)类型。这些是枚举的,第 13 个 SEXP 类型是一个整数 SEXP——因此是13 INTSXP。查看.Internal(inspect(...)) 以获取数字向量、字符向量甚至函数.Internal(inspect(function() {}))。
R 通过定期运行“垃圾收集器”来管理内存,该“垃圾收集器”检查当前是否引用了内存;如果不是,则将其回收以供另一个符号使用。垃圾收集器是“分代的”,这意味着最近分配的内存比旧内存更频繁地检查回收(这是因为,根据经验,变量的半衰期往往很短,例如,在函数调用期间,所以最近分配的内存比使用时间更长的内存更有可能用于回收)。 g0c4 和类似的注释提供了有关 SEXP 所属代的信息。
TR 代表 SEXP 中设置的“位”,表示正在跟踪变量;它是在我们说tracemem(a)时设置的。
其中一些主题在 R 的内部实现文档RShowDoc("R-ints") 和 C 头文件 Rinternals.h 中进行了讨论。