How mmap Improves Sequential Reading in C

Memory-mapped file I/O using the mmap system call is an efficient technique in C that maps a file's contents directly into a process's virtual memory address space. This article explains how mmap works, details the technical reasons it improves sequential reading performance over traditional file access methods, and demonstrates how to implement it in a C program.

Understanding mmap

In POSIX-compliant operating systems, mmap() is a system call defined in <sys/mman.h>. Instead of reading bytes from a file into an application-allocated buffer using read() or fread(), mmap() creates a direct mapping between a file descriptor and the process’s virtual memory pages.

Once mapped, the operating system assigns a pointer to the beginning of the memory region. The application can then read data directly from the pointer as if the entire file were a contiguous array residing in RAM. Actual physical disk reads are handled transparently by the operating system’s page fault mechanism.

The Bottlenecks of Standard Read Operations

Traditional sequential reading using the read() system call suffers from two main performance penalties:

  1. Context Switching Overhead: Every call to read() requires a transition from user mode to kernel mode and back. Reading a large file in small chunks generates thousands of context switches, which consume significant CPU resources.
  2. Double Buffering: When reading a file traditionally, the operating system first transfers data from the storage device into its own page cache (kernel space), and then copies that data into the buffer provided by the application (user space). This extra memory-to-memory copy consumes CPU cycles and memory bandwidth.

How mmap Enhances Sequential Reading

Memory mapping addresses the inefficiencies of conventional I/O in several specific ways:

  • Zero-Copy Access: mmap() eliminates the intermediate copy between kernel space and user space. The process reads directly from the OS page cache through page table entries, avoiding redundant data movement.
  • Minimized System Calls: A file is mapped with a single system call. Once mapped, sequential traversal through the memory buffer requires no further system calls, drastically reducing context-switch overhead.
  • Kernel Read-Ahead Optimization: Modern operating systems monitor memory access patterns. When a program reads sequentially through a mapped region, the kernel anticipates upcoming reads and initiates asynchronous read-ahead operations to load pages into memory before the program requests them.
  • Sequential Access Hints via madvise(): Developers can use the madvise() system call with the MADV_SEQUENTIAL flag to explicitly inform the kernel of sequential access. This prompts the OS to aggressively prefetch data and immediately release pages behind the current read position, keeping the memory footprint low.

Implementing mmap in C

The following example demonstrates how to map a file, read its contents sequentially, and unmap it cleanly:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        fprintf(stderr, "Usage: %s <file_path>\n", argv[0]);
        return EXIT_FAILURE;
    }

    int fd = open(argv[1], O_RDONLY);
    if (fd == -1) {
        perror("Error opening file");
        return EXIT_FAILURE;
    }

    // Get file size
    struct stat sb;
    if (fstat(fd, &sb) == -1) {
        perror("Error getting file size");
        close(fd);
        return EXIT_FAILURE;
    }

    size_t length = sb.st_size;
    if (length == 0) {
        close(fd);
        return EXIT_SUCCESS;
    }

    // Map file into memory
    char *data = mmap(NULL, length, PROT_READ, MAP_SHARED, fd, 0);
    if (data == MAP_FAILED) {
        perror("Error mapping memory");
        close(fd);
        return EXIT_FAILURE;
    }

    // Tell kernel to optimize for sequential access
    madvise(data, length, MADV_SEQUENTIAL);

    // Sequential read traversal
    unsigned long long checksum = 0;
    for (size_t i = 0; i < length; i++) {
        checksum += (unsigned char)data[i];
    }

    printf("Sequential read complete. Checksum: %llu\n", checksum);

    // Cleanup resources
    munmap(data, length);
    close(fd);

    return EXIT_SUCCESS;
}

When to Use mmap

mmap() is particularly advantageous for high-throughput sequential reads of large datasets, such as database storage engines, index parsers, and binary log analyzers. However, for extremely small files or simple linear pipelines involving standard input/output streams, the overhead of setting up page tables and handling page faults may offset the benefits. For sustained sequential reading of moderately large to massive files, mmap remains one of the fastest I/O mechanisms available in C.