第11.5章: 带缓冲与不带缓冲的 I/O
概述
fast_io 提供两个文件类型家族,它们的区别在于一点关键之处:是否带缓冲。选对了,程序可能几毫秒就跑完;选错了,就要跑上几秒。
1. 两个家族
| 类型 | 方向 | 带缓冲? | 最适合 |
|---|---|---|---|
::fast_io::native_file |
输入 / 输出 | 不带(无缓冲) | 大批量数据传输 |
::fast_io::ibuf_file |
仅输入 | 带 | 大量小的读取 |
::fast_io::obuf_file |
仅输出 | 带 | 大量小的写入 |
::fast_io::iobuf_file |
输入 / 输出 | 带 | 混合的小规模读写 |
2. native_file — 不带缓冲(每次操作一次系统调用)
native_file 是 OS 文件句柄之上的一层薄包装。每次 write_some 或 read_some 调用都直接进入内核,你的代码和操作系统之间没有用户态缓冲区。
当你传输大块数据时(例如一次读 1 MiB),这没问题 — 甚至是最理想的。与搬移的数据量相比,系统调用的开销可以忽略。
但当你发起大量小操作时,它慢得灾难性。每次 char_put 或小的 print 都变成一次单独的系统调用,而切入内核的上下文切换主导了运行时间。
#include <fast_io.h>
#include <fast_io_device.h>
int main() {
// Unbuffered: every write is a syscall
::fast_io::native_file obf{"bulk.bin",
::fast_io::open_mode::out | ::fast_io::open_mode::trunc};
// Good: one big syscall for a large block
char const big_buffer[65536]{};
::fast_io::operations::write_all(obf, big_buffer, big_buffer + 65536uz);
// BAD: 65536 individual syscalls!
for (::std::size_t i{}; i < 65536uz; ++i) {
::fast_io::operations::char_put(obf, 'A');
}
}
3. iobuf_file — 带缓冲(用户态批处理)
iobuf_file(以及仅输入/仅输出的兄弟姐妹)用一个用户态缓冲区包裹原生句柄,通常有几 KiB。写入先积累在这个缓冲区里;只有当缓冲区填满时,fast_io 才发起一次大的系统调用把它刷新出去。读取则反过来同理:一次系统调用取回一大块,后续的 char_get 调用直接从缓冲区取。
这把成千上万次微小的系统调用变成少数几次大的系统调用,速度快得多。
#include <fast_io.h>
#include <fast_io_device.h>
int main() {
// Buffered: writes accumulate in user space
::fast_io::obuf_file obf{"fast.bin"};
// Even 65536 char_puts are cheap — they hit the user buffer,
// not the kernel. The buffer flushes automatically when full.
for (::std::size_t i{}; i < 65536uz; ++i) {
::fast_io::operations::char_put(obf, 'A');
}
// Buffer is flushed here when `obf` goes out of scope.
}
对 iobuf_file(双向)来说,输入缓冲区和输出缓冲区是绑定在一起的:只要输入缓冲区需要从文件取更多数据,它就会先刷新任何待输出的内容。这意味着你可以安全地在读写之间交替,而不必手动调用 flush。
4. 概念性基准
下面的概念性数字用来说明差异。在现代 Linux 系统上按每字节一次写入 1 MiB:
| 设备 | 发起的系统调用次数 | 大致耗时 |
|---|---|---|
native_file |
1 048 576 | 约 200–500 ms |
obuf_file |
约 16 次(每填满 64 KiB 缓冲区一次) | 约 1–3 ms |
写的是完全相同的字节,却有100×–500× 的加速。差别纯粹来自系统调用的代价。
对大批量传输(一次读写若干兆字节)来说,两者相当,因为系统调用开销被大量数据摊薄了。差距只在大量小操作时才出现。
5. 并排对比示例
#include <fast_io.h>
#include <fast_io_device.h>
int main() {
// ----- Unbuffered: native_file -----
// Use this when you already have large buffers in hand.
{
::fast_io::native_file src{"big_input.bin", ::fast_io::open_mode::in};
::fast_io::native_file dst{"big_output.bin",
::fast_io::open_mode::out | ::fast_io::open_mode::trunc};
char buffer[131072]; // 128 KiB
while (true) {
char const* read_end = ::fast_io::operations::read_some(src, buffer, buffer + 131072uz);
if (read_end == buffer) break; // EOF
::fast_io::operations::write_all(dst, buffer, read_end);
}
}
// ----- Buffered: ibuf_file / obuf_file -----
// Use this when operating on many small items (lines, integers, etc.)
{
::fast_io::ibuf_file ibf{"numbers.txt"};
::fast_io::obuf_file obf{"sum.txt"};
using namespace ::fast_io::iomnp;
::std::int64_t sum{};
::std::int64_t value{};
while (scan(ibf, value)) {
sum += value;
}
println(obf, "Sum: ", sum);
}
// ----- Buffered bidirectional: iobuf_file -----
// Use when you need both reading and writing on the same file.
{
::fast_io::iobuf_file iof{"patched.bin",
::fast_io::open_mode::in | ::fast_io::open_mode::out};
// Read a header
::std::uint32_t magic{};
using namespace ::fast_io::iomnp;
scan(iof, magic);
// Jump to offset 1024 and patch a field
::fast_io::operations::io_stream_seek(iof, 1024);
println(iof, "patched");
}
}
6. 何时用哪个
| 场景 | 使用 |
|---|---|
| 以大块复制多兆字节的数据 | native_file(批量传输,没有缓冲区开销) |
用 getline 逐行读文件 |
ibuf_file |
| 写上百万条小记录、整数或字符 | obuf_file |
| 先读再写同一个文件(例如修改文件头) | iobuf_file |
| 在循环里逐字符或逐字节写 | obuf_file — 绝不要用 native_file |
| 内存映射 / 无复制的大块 I/O | native_file + file_loader(见第11.8章) |
要点
::fast_io::native_file不带缓冲:每次读写都是一次系统调用。大批量传输时快,大量小操作时慢得灾难性。::fast_io::iobuf_file(以及ibuf_file/obuf_file)带缓冲:在用户态缓冲区里把小的操作攒起来,满了用一次系统调用刷出 — 对小规模 I/O 快上数量级。- 仅输入用
ibuf_file,仅输出用obuf_file,双向 I/O 用iobuf_file。 - 经验法则:如果你单次读写的规模小于缓冲区(通常 4–64 KiB),就用带缓冲的类型。如果你本来就在按几 KiB 的块搬数据,
native_file能省掉多一次缓冲区复制。 - 绝不要在循环里用
native_file配char_put— 改用obuf_file。