第11.5章: 带缓冲与不带缓冲的 I/O

English

概述

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章)

要点