Multi-Gen LRU (MGLRU): Why Linux Replaced Its 20-Year-Old Page Reclaim Algorithm

2026-09-02

For two decades, Linux managed page reclaim with a pair of lists per memory zone: active and inactive. On memory pressure, kswapd scanned the inactive list looking for cold pages. Accessed pages got promoted to active; unaccessed ones got evicted. Simple, but broken at scale: with 100+ GB of RAM, the scanner touched millions of PTEs sampling access bits, and the two-bucket resolution was too coarse to tell "used 5 minutes ago" from "used 5 hours ago." Result: hot pages evicted, cold pages retained, and reclaim latency spikes visible in tail latencies.

MGLRU (merged in Linux 6.1, 2022) replaces the two lists with a generational structure: typically 4 generations per memory type (anon/file), giving 8 buckets instead of 2. Each page has a small gen counter encoded in page->flags. On access, the page is promoted to the youngest generation; during reclaim, the oldest generation is evicted first. The key trick is bulk aging: instead of scanning individual pages, MGLRU walks page tables in sequence and uses the CPU's Accessed bit across whole PTE ranges at once, exploiting spatial locality in the mm_struct.

Concrete example. Meta reported using MGLRU on their web tier: a 512GB fleet running memcached-style workloads. Under old LRU, refault rate (pages evicted then re-read from disk within seconds) hit 8-12% during traffic spikes. With MGLRU, refaults dropped below 2% because the finer-grained aging correctly identified the working set. Same workload, same hardware, ~15% p99 latency improvement. Android also enabled MGLRU by default in 13+ because it reduced swap thrashing on 4GB phones.

The rule of thumb. Reclaim overhead in the old LRU scales roughly as O(RSS / working_set_size) — the more your working set fits, the more time you waste scanning warm pages. MGLRU's bulk PTE walk drops the constant factor by ~10x: instead of one ptep_test_and_clear_young() per candidate, one walk clears the A-bit across 512 PTEs (one 2MB region) at a time. On a 128GB machine, aging a full pass drops from ~300ms to ~30ms.

Tuning knobs (all under /sys/kernel/mm/lru_gen/): enabled toggles it; min_ttl_ms sets the minimum age before a page can be evicted (useful to prevent thrashing); the debugfs interface /sys/kernel/debug/lru_gen lets you force aging or eviction for testing. Watch /proc/vmstat's pgsteal_* and pgrefill_* counters — if refills exceed steals by wide margins, your working set exceeds RAM regardless of algorithm.

Key Takeaway: MGLRU replaced Linux's binary active/inactive lists with generational buckets and bulk PTE-range aging, giving reclaim finer resolution and ~10x lower scanning overhead — turning "cold pages" from a guess into a measurement.

All newsletters