为什么 vec![0, <super large number>] 默认值为 0 时内存效率如此之高?

Why is vec![0, <super large number>] so memory-efficient when the default value is 0?

我在 Rust 中处理大量数据,在评估数据结构的内存使用情况时,我偶然发现了一些令人惊讶的结果。

首先,我尝试通过将零推入向量来手动填充向量:

let mut arr = vec![];

for _ in 0..(4_294_967_294 as u32) {
    arr.push(0);
}

一段时间后,我会说,我的计算机 运行 可用内存不足,进程被 OS 终止。

但是,如果我使用宏初始化来初始化向量,行为会发生变化:

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();
    }
}

即使我用一个实际的 u64 值填充了 10 亿个条目并从数组中读出 运行dom 值(主要是零,我只是试图在此处排除对整个数组的编译器优化),我的电脑内存没有溢出。

每个 jemalloc 的内存使用情况是这样的(请注意,我的计算机只安装了 16 GB 的 RAM):

allocated 16777216.05 MiB resident 16777223.02 MiB

... 而我的 OS 在代码末尾报告了大约 8000M(以 htop 测量)的最大值。

奇怪的是,如果我使用除 0 以外的任何其他默认值(无论是 1 还是 100),宏在完成向量创建之前就会耗尽内存,因此它肯定与初始值为 0 有关。

我想知道宏做了什么来保持生成的数据结构如此高效的内存?数组中的元素不是真正创建的吗?如果不是,那么它如何才能与我一起从向量中读取 运行dom 索引?

我已经检查了 documentation,虽然它只是说它依赖于 Clone 类型的默认元素,这对基本类型没有任何意义。

为向量分配内存时,有一些分配 built-ins 可以使用。当向量的类型为数值且给定的初始值为零时,使用__rust_alloc_zeroed

在 Unix-compatible 系统上,此分配器函数的默认实现可以使用 calloc()posix_memalign()

calloc()保证分配归零; posix_memalign() 没有。如果使用后者,Rust 分配器会将内存本身归零。

鉴于您观察到的行为,唯一合理的解释是使用了 calloc()。由于使用 previously-freed 内存的库无法满足请求(分配肯定太大),因此将此请求传递给内核,内核在进程页面 table 中为请求的分配创建条目.

但是,OS 不必实际为分配中的每个页面分配一个物理内存区域。它可以将此推迟到以后,一种称为 overcommitment.

的技术

如果物理内存尚未支持,则读取或写入分配区域中的地址将触发页面错误。发生此故障时,内核通过为访问的页面分配内存区域来解决它。

所有这一切的最终结果是,如果您创建一个初始值为零的数字类型的向量,则该分配最初实际使用的系统内存很少。几乎所有的分配都在还没有支持系统内存的页面内,类似于稀疏文件中的一个洞。当您写入向量时,系统将开始为分配分配物理内存,并且您使用的内存(and/or used swap)将开始增加。