Skip to content

I/O 性能 ​

本文所有示例均可在本仓库中直接编译运行(Linux)。I/O 的性能模型与计算完全不同:瓶颈在系统调用次数、页缓存与拷贝次数,而不是指令吞吐。

心智模型:一次 read() 到底发生了什么 ​

read(fd, buf, n) 的内核路径大致是:

  1. 陷入内核(上下文切换,几百 ns 起步);
  2. 在页缓存(page cache)中查找文件对应的页——命中则无磁盘动作;
  3. 把页内数据拷贝到用户缓冲区 buf;
  4. 返回用户态。

两个直接推论:

  • 系统调用是固定成本。传 1 字节和传 1 MiB 的陷入开销几乎相同,所以单次传输量越小,单位数据付出的固定成本越高;
  • 顺序读热文件时磁盘不是瓶颈,页缓存命中后拼的是拷贝与调用开销。要测真实设备必须 O_DIRECT 绕过页缓存——那是另一类实验。

读路径对照:read vs pread vs mmap ​

examples/07-io-performance/src/mmap_vs_read.cpp 用同一份页缓存驻留文件对照三条路径:

路径机制特点
read() 循环每次调用拷贝一段进用户缓冲通用;顺序偏移由 fd 维护
pread() 循环带显式偏移的读,不动 fd 偏移多线程可共享一个 fd,无需加锁
mmap + 顺序触摸建立页表映射,按页缺页载入无前置拷贝;访问即 I/O

关键结论(与直觉相反的部分):顺序全扫描时 mmap 不会快一个数量级——最终读的页完全相同。本仓库实测(32 MiB 缓存驻留文件)mmap 约快 20%,来自省掉的内核→用户拷贝与更少的调用次数。mmap 真正的优势场景是:

  • 随机访问:直接按地址取数,无需 seek+read 组合;
  • 共享映射:多进程映射同一文件页,省去重复读取与拷贝;
  • 大文件稀疏访问:只有被触摸的页才产生 I/O。

示例用 FNV-1a 校验和验证三条路径读到的内容逐字节一致——对照实验的前提是"做的是同一件事"。

写路径:系统调用开销的极端演示 ​

examples/07-io-performance/src/buffered_write.cpp 把同一份 1 MiB 数据用三种方式写出:

方式系统调用次数本仓库实测
逐字节 write(fd, &b, 1)1,048,576~0.7 MiB/s
4 KiB 缓冲块256~930 MiB/s
单次大块写1~1 GiB/s

1300 倍的差距全部来自系统调用固定成本。这就是 fwrite、std::ofstream、日志库默认带缓冲的原因,也是"每条日志一次 write"在高频路径上必然后悔的原因。

规范库与基准 ​

include/hpc/io_utils.hpp 提供模块共用的 POSIX 封装:RAII FileDescriptor(含 O_CREAT 强制 mode 参数,避免经典的缺 mode 未定义行为)、RAII MmapView(MAP_SHARED)、pread 整读、确定性图案临时文件。

examples/07-io-performance/bench/io_bench.cpp 用 Google Benchmark 在 1–128 MiB 尺寸上复测三条读路径,每个尺寸只创建一次临时文件(setup 不计入计时)。

边界说明 ​

  • 本模块是 Linux/POSIX 专属(mmap/pread/mkstemp),非 Linux 平台配置时自动跳过;
  • io_uring(Linux 5.1+ 的异步 I/O 接口,靠提交/完成队列摊薄系统调用)值得单独成篇,本仓库暂不提供示例——它依赖 liburing 且收益场景集中在高并发小 I/O,与本文的缓存驻留实验不同题;
  • 测设备真实带宽/延迟需要 O_DIRECT + 对齐缓冲 + 控制预读(posix_fadvise),并注意 SSD 写放大与 GC 干扰。

基于 MIT 许可证发布。