“Just mmap it, it’s faster” is one of those pieces of advice that’s right often
enough to be dangerous. Memory mapping can be a big win or a quiet loss, and the
difference comes down to two costs you can reason about directly: syscalls and
page faults.
What read() Actually Does
A read() copies bytes from the kernel page cache into your buffer:
char buf[64 * 1024];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
process(buf, n);
}Every call is a syscall — a user→kernel transition — plus a memcpy from the page cache into your buffer. For a large file you pay that crossing many times, and you pay for the copy every time.
What mmap Does Instead
mmap maps the file’s pages into your address space. There’s no per-chunk
syscall and no copy into a user buffer — you read the page cache directly:
struct stat st;
fstat(fd, &st);
char *data = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
for (size_t i = 0; i < st.st_size; i++)
process_byte(data[i]); // first touch of each page faults it in
munmap(data, st.st_size);The cost moves from syscalls to page faults. The first access to each 4 KB page traps into the kernel, which maps the cached page into your tables. No copy, but a fault per page.
The Tradeoff in One Picture
read(): [syscall][copy] [syscall][copy] [syscall][copy] ...
└─ fixed cost per chunk, data copied into your buffer
mmap(): [fault] . . . . [fault] . . . . [fault] . . . .
└─ one fault per page, zero copy, paid lazily on touchSo the question is just: which is cheaper for your access pattern?
When mmap Wins
- Random access into a large file.
read/lseekper location is brutal; mapped memory makes it a pointer dereference. - Repeated passes over the same data. Pages fault in once and stay warm; no repeated copies.
- Sharing the same file read-only across processes — they share the cached pages.
When read() Wins
- Streaming once, front to back. You fault every page exactly once and never
reuse it — that’s the same work as a copy, minus the prefetch friendliness of a
plain sequential read.
read()with a decent buffer (orreadahead) is simpler and often faster. - Small files, where
mmap/munmapsetup and TLB churn outweigh any savings. - Pipes, sockets, and other non-seekable fds, which you can’t map at all.
Don’t Forget the Failure Modes
A read() returns -1 and an errno you can handle. A truncated mmap region
hands you a SIGBUS when you touch a page past the file’s real end — an
async failure that’s far easier to get wrong. If the file can change size under
you, that risk is real.
Takeaways
read()trades syscalls + a copy;mmaptrades page faults + no copy.- Map it for random access or repeated passes; stream it with
read()for a single sequential pass or small files. mmapcan’t map pipes/sockets and turns I/O errors intoSIGBUS— handle that before reaching for it.- Measure with your real access pattern; the winner flips depending on whether you touch each byte once or many times.