零初始化内存访问为何产生双倍数量的页错误?
Rust数组零初始化与非零初始化的性能差异解析
测试代码
use std::time::Instant; fn main() { let mut array = vec![0i32; 64 * 1024 * 1024]; let start = Instant::now(); workload(&mut array); let dur = start.elapsed().as_millis(); println!("duration: {}ms", dur); } fn workload(arr: &mut [i32]) { for i in 0..arr.len() { arr[i] *= 3; } }
测试现象
使用--release模式运行时,零初始化数组的耗时为278ms;若将数组初始化值改为1,耗时骤降至17ms。
Perf分析结果
通过perf工具统计到:
- 零初始化场景:131K次minor page faults
- 1初始化场景:65K次minor page faults
注:在4KiB页大小的机器上,256MB(6410241024个i32元素)的数组共需65536个物理页。
1. 零初始化场景为何会产生双倍数量的页错误?
这是内核零页优化与**写时复制(COW)**机制共同作用的结果:
- 当使用
vec![0; N]创建数组时,Rust会调用alloc_zeroed分配内存,内核会将虚拟地址空间映射到只读共享零页,此时并未实际分配私有物理页,也不会触发页错误。 - 进入
workload执行写操作时,每个物理页会触发两次minor page fault:- 第一次访问页内元素时,读操作会触发minor fault,将虚拟页正式映射到共享零页;
- 执行
arr[i] *=3的写操作时,由于共享零页是只读的,内核会触发第二次minor fault,为该页分配私有物理页、复制零页内容并标记为可写(COW机制)。
- 而非零初始化的
vec![1; N]会在数组创建阶段,通过memset直接向每个物理页写入初始值1,此时每个页仅触发一次minor fault(分配并初始化物理页),后续workload的写操作无需再触发页错误。
2. 页错误差异是速度大幅提升的核心原因吗?
是的,页错误处理是典型的内核态耗时操作:每次页错误需要陷入内核,执行页表修改、物理页分配/复制等逻辑,单次处理耗时可达微秒级。
- 零初始化场景中,131K次页错误的处理时间占据了278ms耗时的绝大部分;
- 非零初始化场景中,
workload仅需执行纯用户态的内存写操作,256MB内存的连续写入耗时仅需十数毫秒,与测试结果一致。
3. 如何测量页错误处理消耗的时间?
可以通过以下工具和方法:
time命令:使用/usr/bin/time -v ./your_program,输出中的System time字段对应内核态执行时间,其中大部分为页错误处理耗时;同时可直接查看Minor (reclaiming a frame) page faults的次数。perf工具:- 统计页错误次数及耗时占比:
perf stat -e minor-faults,page-faults,cpu-clock ./your_program,可直观看到页错误次数与总耗时的关联; - 分析内核态调用:通过
perf record ./your_program捕获采样数据,再用perf report查看内核函数的耗时占比,其中与页错误相关的内核函数(如handle_mm_fault)的耗时即为页错误处理时间。
- 统计页错误次数及耗时占比:
- 内核跟踪工具:若需更精确的单次页错误耗时,可使用
ftrace跟踪内核的page_fault事件,结合时间戳计算单次处理的耗时(该方法需内核支持,操作复杂度较高)。
内容的提问来源于stack exchange,提问作者tverghis
相关产品推荐
相关产品推荐

