C++笔记之大块顺序写

code review!
文章目录
- C++笔记之大块顺序写
-
- [1 顺序写](#1 顺序写)
-
- [1.1 含义](#1.1 含义)
- [1.2 举例](#1.2 举例)
- [1.3 示例代码](#1.3 示例代码)
- [1.4 要点](#1.4 要点)
- [2 大块顺序写](#2 大块顺序写)
-
- [2.1 含义](#2.1 含义)
- [2.2 目的](#2.2 目的)
- [2.3 举例](#2.3 举例)
- [2.4 实践建议](#2.4 实践建议)
- [3 小块写与大块写对比](#3 小块写与大块写对比)
-
- [3.1 示例代码](#3.1 示例代码)
- [3.2 说明](#3.2 说明)
- [4 刷盘](#4 刷盘)
-
- [4.1 含义](#4.1 含义)
- [4.2 特点](#4.2 特点)
- [4.3 flush 与 fsync 的区别](#4.3 flush 与 fsync 的区别)
- [4.4 示例](#4.4 示例)
- [4.5 刷盘策略](#4.5 刷盘策略)
- [5 总结](#5 总结)
1 顺序写
1.1 含义
含义:将原本按随机地址(乱序、跳跃位置)写入文件的操作,改为按文件地址从小到大依次顺序写入,避免磁盘/文件指针频繁跳转,从而提高写入性能(尤其对机械磁盘效果明显,因为顺序写减少了寻道时间)。
1.2 举例
- 随机写:先写第 100 字节,再写第 10 字节,再写第 500 字节。
- 顺序写:先写第 0~99 字节,再写第 100~199 字节,依次递增。
1.3 示例代码
cpp
#include <fstream>
int main() {
// 随机写:每次都要 seekp 跳到不同位置,乱序,效率低
{
std::ofstream f("random.bin", std::ios::binary);
f.seekp(300); f.write("C", 1);
f.seekp(0); f.write("A", 1);
f.seekp(150); f.write("B", 1);
}
// 顺序写:定位一次(或不定位),写指针自动后移,效率高
{
std::ofstream f("sequential.bin", std::ios::binary);
f.write("A", 1);
f.write("B", 1);
f.write("C", 1);
}
return 0;
}
1.4 要点
- 顺序写只需一次定位,甚至不需要定位,后续写指针自动递增。
- 随机写每次都要
seekp跳转到不同位置,增加磁盘寻道开销。
2 大块顺序写
2.1 含义
含义:将数据攒够一定大小(大块,如几十KB~几MB)后一次性顺序写入,而不是频繁地写小块数据。
2.2 目的
- 减少写入次数,包括系统调用次数和 IO 请求次数。
- 充分利用磁盘的顺序写带宽,提高吞吐量。
- 降低每次调用的固定开销,例如用户态与内核态切换、加锁和元数据更新。
2.3 举例
cpp
// 小块写(低效):多次调用,每次只写一点
for (int i = 0; i < 10000; i++) {
f.write(&data[i], 1); // 每次写 1 字节
}
// 大块写(高效):攒够数据后一次性写入
f.write(buffer, buffer_size); // 一次写几 MB
2.4 实践建议
- 在内存中维护一个写缓冲区,大小可取 64KB 到数 MB,写满后再一次性写出。
- 程序结束或需要保证数据可见时,把缓冲区中剩余的数据写出。
- 可以用
rdbuf()->pubsetbuf()为ofstream设置更大的缓冲区。这个调用要在打开文件前完成,行为依赖具体实现。
3 小块写与大块写对比
3.1 示例代码
cpp
#include <fstream>
#include <chrono>
#include <iostream>
#include <vector>
int main() {
const int N = 1000000; // 1M次
char c = 'A';
// 小块写:每次写1字节,共写N次
{
std::ofstream f("small.bin", std::ios::binary);
auto start = std::chrono::steady_clock::now();
for (int i = 0; i < N; i++) {
f.write(&c, 1); // 频繁小块写
}
auto end = std::chrono::steady_clock::now();
std::cout << "小块写耗时: "
<< std::chrono::duration<double>(end - start).count()
<< " 秒\n";
}
// 大块写:攒满缓冲区后一次性写入
{
std::ofstream f("large.bin", std::ios::binary);
std::vector<char> buffer(N, c); // 一次准备好所有数据
auto start = std::chrono::steady_clock::now();
f.write(buffer.data(), buffer.size()); // 一次性大块写
auto end = std::chrono::steady_clock::now();
std::cout << "大块写耗时: "
<< std::chrono::duration<double>(end - start).count()
<< " 秒\n";
}
return 0;
}
3.2 说明
- 两者写入的数据量相同,都是 N 字节。小块写调用 N 次
write,大块写只调用 1 次。 - 大块写通常明显更快。
std::ofstream自带用户态缓冲区,所以小块写的耗时主要是函数调用开销。如果直接调用系统调用write(),或者每次写完都flush(),两者的差距会大得多。
4 刷盘
4.1 含义
刷盘是把数据从内存缓冲区真正写入物理磁盘的过程。数据从程序到磁盘要经过两层缓存:
- 用户态缓冲区,例如
ofstream和FILE*的缓冲区。 - 操作系统页缓存(page cache)。
4.2 特点
- 平时调用
write()时,数据通常只到达内存缓存,没有真正落盘。这样速度快,但断电会丢数据。 - 刷盘通过
flush()、fsync()等操作强制写入,保证数据持久化,但会增加 IO 等待,降低性能。
4.3 flush 与 fsync 的区别
flush()(或fflush)只把用户态缓冲区的数据交给操作系统,不保证数据已经落到磁盘。fsync(fd)(Linux/POSIX)会要求操作系统把该文件的页缓存写到存储设备,这才是真正的持久化。Windows 对应的是FlushFileBuffers。
4.4 示例
cpp
#include <cstdio>
#include <unistd.h> // fsync, fileno
int main() {
FILE* fp = std::fopen("data.bin", "wb");
const char data[] = "hello";
std::fwrite(data, 1, sizeof(data), fp); // 写入用户态缓冲区
std::fflush(fp); // 刷到操作系统页缓存
fsync(fileno(fp)); // 强制页缓存落盘
std::fclose(fp);
return 0;
}
使用 ofstream 时,f.flush() 只完成第一步。需要真正落盘时,要改用文件描述符或 FILE*,再调用 fsync。
4.5 刷盘策略
- 不要每写一小块就刷盘,这会抵消大块顺序写带来的收益。
- 常见做法是攒够一个大块后写入,再按数据重要性决定刷盘时机。可以每写若干 MB 刷一次,也可以按时间间隔刷,关键数据则写完立即刷。
5 总结
- 顺序写减少磁盘寻道和随机 IO。
- 大块写减少系统调用和 IO 次数,提升吞吐量。
- 刷盘保证数据持久化,但代价是性能,要在可靠性和效率之间取舍。
- 高性能写入的常用组合是:内存中攒数据,大块顺序写入,再按策略批量刷盘。