Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 6: The operating system

Page faults and how to avoid them

Lesson · 10 min

The question

The first few thousand messages after start-up are slow, and occasionally later in the day a message takes hundreds of microseconds for no visible reason. Could page faults explain either?

What a page fault is

When a thread touches a virtual page that has no valid mapping with the needed permissions, the processor traps into the kernel. The kernel decides whether the access is legal and, if so, makes it valid and resumes the thread at the same instruction.

  • Minor fault: the data is already in memory or needs none, and the kernel only has to set up the mapping. The first write to freshly allocated memory is the commonest case: the kernel takes a free physical page, zeroes it and maps it.
  • Major fault: the data must be read from disk, from a swapped-out page or a memory-mapped file. Milliseconds.

A minor page fault on the first write to freshly allocated memory. Nothing is wrong, but the thread stops while the kernel finds a physical page, zeroes it and maps it. Each new 4 KB page pays this once.

  1. Step 1 / 5
    your threadkernelwrite buf[0]

    The thread writes to a page it has never touched.

  2. Step 2 / 5
    your threadkernelwrite buf[0]trap

    The CPU traps into the kernel.

  3. Step 3 / 5
    your threadkernelwrite buf[0]stoppedtrapallocate + zero 4 KB

    The kernel finds a free physical page and zeroes it.

  4. Step 4 / 5
    your threadkernelwrite buf[0]stoppedtrapallocate + zero 4 KBmap, return

    It maps the page and returns.

  5. Step 5 / 5
    your threadkernelwrite buf[0]stoppedwrite retriedtrapallocate + zero 4 KBmap, return

    The write is retried and succeeds. The thread was stopped for all of it.

A minor fault costs on the order of a microsecond or more including the trap and the zeroing. That is small once and enormous on a path budgeted in single-digit microseconds.

Why first touch is slow

malloc or new for a large block, and mmap, return address ranges, not memory. Linux allocates physical pages lazily, on first touch. So a vector reserved at start-up has its capacity in virtual terms only; the first write to each 4 KB page faults. That explains the slow start in the question: every pool, ring buffer and table touched for the first time by early messages faults page by page.

Later in the day

Faults later on have other sources:

  • Memory that was never touched until a rare path or a busy period reached it: the last part of a pool, a rarely used table.
  • Memory returned and re-requested. free may give memory back to the kernel (glibc trims the heap and unmaps large blocks); the next allocation faults again.
  • Copy-on-write. After fork, parent and child share pages read-only; the first write by either to each shared page faults and copies it. Forking a large process to write a snapshot is a classic source of mysterious stalls.
  • Swapping and reclaim. Under memory pressure the kernel may evict pages, including file-backed code pages of your binary, which then fault back from disk. Major faults in the middle of the day point here.
  • Transparent huge page activity (previous lesson).

Keeping faults off the hot path

  1. Pre-fault everything at start-up. After allocating pools, rings and tables, write to every page (writing a zero to each page is enough; memset the whole region is simplest), so the faults happen before trading.
  2. Lock memory in RAM. mlockall(MCL_CURRENT | MCL_FUTURE) stops the kernel from paging anything out, including later allocations, and with MCL_FUTURE new mappings are faulted in when created. It needs the right resource limits (RLIMIT_MEMLOCK) or privileges.
  3. MAP_POPULATE on mmap faults the whole mapping at creation.
  4. Do not return memory. Configure the allocator not to trim or unmap (glibc's mallopt(M_TRIM_THRESHOLD, ...) and M_MMAP_THRESHOLD, or the equivalents in other allocators), and avoid munmap on live systems.
  5. No fork of the trading process while it is running.
  6. Warm up the code paths too: running representative messages through the system before the open faults in code pages and trains caches and branch predictors.

Measuring

perf stat -e page-faults,major-faults over a run counts them. perf record -e page-faults shows where they happen. In production, a per-thread fault counter (getrusage(RUSAGE_THREAD) reports minor and major faults) read periodically can be exported as a metric, so a change that reintroduces faults on the hot path is caught the day it ships.

void prefault(void* p, std::size_t n) {
    auto* b = static_cast<volatile char*>(p);
    for (std::size_t off = 0; off < n; off += 4096) b[off] = 0;   // one write per page
}

Check yourself

Question 1 of 3

Why does the first write to memory from a large new often cost a page fault?

Question 2 of 3

What does mlockall(MCL_CURRENT | MCL_FUTURE) do?

Question 3 of 3

A process forks a child to write a snapshot to disk. Why might the parent's latency spike afterwards?

In the interview

Say your own answer out loud first, then open the one we would give.

What is a page fault and why does it matter for latency?

A trap into the kernel when a thread touches a page without a valid mapping. A minor fault only needs a mapping set up, often with a page zeroed, around a microsecond or more; a major fault needs disk I/O. On a hot path budgeted in microseconds, even minor faults are visible spikes.

How do you avoid page faults on the hot path?

Allocate everything at start-up and touch every page before trading, or use MAP_POPULATE; lock memory with mlockall; stop the allocator returning memory to the kernel; never fork the trading process; and warm up the code paths with representative traffic. Then monitor per-thread fault counts in production.

Why can fork cause latency problems?

Parent and child share pages copy-on-write, so the first write by the parent to each page after the fork faults and copies it. A large process that keeps writing pays thousands of faults, plus the page-table copy at fork time.