# mmap vs read(): When Mapping a File Actually Wins

> mmap is sold as the fast way to read files, but it loses as often as it wins. The deciding factor is page faults versus syscalls — and whether you touch the data once or many times.

URL: https://harshith.in/blog/mmap-vs-read-large-files/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: linux, performance, systems

"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.
