Ch11.5: Buffered vs Unbuffered I/O

Overview

fast_io provides two families of file types that differ in a single, critical way: buffering. Choosing the right one can be the difference between a program that runs in milliseconds and one that takes seconds.

1. The Two Families

Type Direction Buffered? Best For
::fast_io::native_file In / Out No (unbuffered) Large bulk transfers
::fast_io::ibuf_file Input only Yes Many small reads
::fast_io::obuf_file Output only Yes Many small writes
::fast_io::iobuf_file In / Out Yes Mixed small reads and writes

2. native_file — Unbuffered (one syscall per operation)

native_file is a thin wrapper over the OS file handle. Every write_some or read_some call goes directly to the kernel. There is no user-space buffer between your code and the OS.

This is fine — even ideal — when you are transferring large blocks of data (e.g. reading a 1 MiB chunk at a time). The syscall overhead is negligible compared to the amount of data moved.

It is disastrously slow when you issue many small operations. Each char_put or small print becomes a separate syscall, and context-switching into the kernel dominates the runtime.


#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 — Buffered (user-space batching)

iobuf_file (and its input/output-only siblings) wraps the native handle with a user-space buffer, typically several KiB in size. Writes accumulate in this buffer; only when the buffer fills does fast_io issue a single large syscall to flush it. Reads work the same way in reverse: one syscall fetches a large block, and subsequent char_get calls serve from the buffer.

This turns thousands of tiny syscalls into a handful of large ones, which is dramatically faster.


#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.
}

For iobuf_file (bidirectional), the input and output buffers are tied together: whenever the input buffer needs to fetch more data from the file, it first flushes any pending output. This means you can safely alternate between reading and writing without manually calling flush.

4. Conceptual Benchmark

The following conceptual numbers illustrate the difference. Writing 1 MiB one byte at a time on a modern Linux system:

Device Syscalls Issued Approximate Time
native_file 1 048 576 ~200–500 ms
obuf_file ~16 (one per 64 KiB buffer fill) ~1–3 ms

That is a 100×–500× speedup for the exact same bytes written. The difference is purely the cost of syscalls.

For large bulk transfers (reading or writing megabytes at a time), the two are comparable because the syscall overhead is amortized over a lot of data. The gap only appears with many small operations.

5. Side-by-Side Example


#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. When to Use Which

Scenario Use
Copying multi-megabyte blobs in large chunks native_file (bulk transfer, no buffer overhead)
Reading a file line by line with getline ibuf_file
Writing millions of small records, integers, or characters obuf_file
Reading then writing to the same file (e.g. patching a header) iobuf_file
Single-character or single-byte writes in a loop obuf_filenever native_file
Memory-mapped / zero-copy bulk I/O native_file + file_loader (see Ch11.8)

Key takeaways