我正在处理 Rust 中的大量数据,在评估数据结构的内存使用情况时,我偶然发现了一些令人惊讶的结果。
首先,我尝试通过将零推入其中来手动填充向量:
let mut arr = vec![];
for _ in 0..(4_294_967_294 as u32) {
arr.push(0);
}
Run Code Online (Sandbox Code Playgroud)
一段时间后,不出所料,我会说,我的计算机耗尽了可用内存,并且该进程被操作系统终止了。
但是,如果我使用宏初始化来初始化向量,行为会发生变化:
let mut arr = vec![0; 2_147_483_647_000];
for i in 1..1_000_000_000 {
arr[i-1] = rng.next_u64();
let sample = rng.next_u32();
let res = arr[sample as usize];
if i % 10000000 == 0 {
print!("After {} ", i);
print!("Btw, arr[{}] == {} ", sample, res);
print_allocated_memory();
}
}
Run Code Online (Sandbox Code Playgroud)
尽管我用实际的 u64 值填充了 10 亿个条目,并从数组中读出了随机值(大部分是零,我只是试图在此处排除整个数组的编译器优化),但我的计算机内存并没有溢出。
内存使用情况jemalloc如下(请注意,我的计算机仅安装了 16 GB RAM):
allocated 16777216.05 MiB resident 16777223.02 MiB
Run Code Online (Sandbox Code Playgroud)
...而我的操作系统在代码末尾报告了最大大约 8000M(以 htop 测量)。
奇怪的是,如果我使用 0 以外的任何其他默认值(无论是 1 还是 100),宏在完成向量创建之前就会耗尽内存,因此它肯定与 init 值为 0 有关。
我想知道宏如何保持结果数据结构的内存效率?数组中的元素不是真的被创建了吗?如果不是,那么它如何与我一起从向量中读出随机索引呢?
我已经检查过文档,尽管它只说它依赖于 type 的默认元素Clone,这对于原始类型来说并没有任何意义。
为向量分配内存时,可以使用一些分配内置函数。当向量的类型为数字且给定的初始值为零时,__rust_alloc_zeroed使用 。
在 Unix 兼容系统上,此分配器函数的默认实现可以使用calloc()或posix_memalign()。
calloc()保证分配归零;posix_memalign()才不是。如果使用后者,Rust 分配器会将内存本身清零。
鉴于您观察到的行为,唯一合理的解释是calloc()使用了该行为。由于使用先前释放的内存的库无法满足该请求(分配肯定太大),因此该请求被传递到内核,内核在进程的页表中为请求的分配创建条目。
但是,操作系统不必实际为分配中的每个页面分配物理内存区域。它可以将其推迟到以后,一种称为过度承诺的技术。
如果尚未得到物理内存的支持,则读取或写入分配区域中的地址将触发页面错误。当发生此错误时,内核通过将内存区域分配给访问的页面来解决它。
所有这一切的最终结果是,如果您创建一个初始值为零的数字类型向量,则该分配最初实际使用的系统内存非常少。几乎所有分配都在还没有支持系统内存的页面内,类似于稀疏文件中的漏洞。当您写入向量时,系统将开始为分配分配物理内存,并且您使用的内存(和/或使用的交换)将开始增加。