Quant developer HFT course · Module 6: The operating system
Page faults and how to avoid them
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.
-
Step 1 / 5
The thread writes to a page it has never touched.
-
Step 2 / 5
The CPU traps into the kernel.
-
Step 3 / 5
The kernel finds a free physical page and zeroes it.
-
Step 4 / 5
It maps the page and returns.
-
Step 5 / 5
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.
freemay 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
- Pre-fault everything at start-up. After allocating pools, rings and tables, write to every page (writing a zero to each page is enough;
memsetthe whole region is simplest), so the faults happen before trading. - Lock memory in RAM.
mlockall(MCL_CURRENT | MCL_FUTURE)stops the kernel from paging anything out, including later allocations, and withMCL_FUTUREnew mappings are faulted in when created. It needs the right resource limits (RLIMIT_MEMLOCK) or privileges. MAP_POPULATEonmmapfaults the whole mapping at creation.- Do not return memory. Configure the allocator not to trim or unmap (glibc's
mallopt(M_TRIM_THRESHOLD, ...)andM_MMAP_THRESHOLD, or the equivalents in other allocators), and avoidmunmapon live systems. - No
forkof the trading process while it is running. - 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?
Physical pages are allocated, zeroed and mapped when first touched. Each 4 KB page pays once.
Question 2 of 3
What does mlockall(MCL_CURRENT | MCL_FUTURE) do?
It pins the process's memory in RAM. With MCL_FUTURE, new mappings are locked, and therefore faulted in, as they are created.
Question 3 of 3
A process forks a child to write a snapshot to disk. Why might the parent's latency spike afterwards?
After fork, every page the parent writes must be copied on its first write. With a large, busy address space that is many faults.
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.