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_file — never native_file |
| Memory-mapped / zero-copy bulk I/O | native_file + file_loader (see Ch11.8) |
Key takeaways
::fast_io::native_fileis unbuffered: every read or write is a syscall. Fast for large bulk transfers, catastrophically slow for many small operations.::fast_io::iobuf_file(andibuf_file/obuf_file) are buffered: they batch small operations in a user-space buffer and flush with a single syscall when full — orders of magnitude faster for small I/O.- Use
ibuf_filefor input-only,obuf_filefor output-only,iobuf_filefor bidirectional I/O. - Rule of thumb: if your individual read/write size is smaller than the buffer (typically 4–64 KiB), use a buffered type. If you are already moving data in multi-KiB chunks,
native_fileavoids the extra buffer copy. - Never use
native_filewithchar_putin a loop — useobuf_fileinstead.