“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:

c
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:

c
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

text
   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 touch

So the question is just: which is cheaper for your access pattern?

When mmap Wins

  • Random access into a large file. read/lseek per 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 (or readahead) is simpler and often faster.
  • Small files, where mmap/munmap setup 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; mmap trades page faults + no copy.
  • Map it for random access or repeated passes; stream it with read() for a single sequential pass or small files.
  • mmap can’t map pipes/sockets and turns I/O errors into SIGBUS — 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.