Code walkthrough

Hash, Speculate and Win

Revelator

Revelator makes the physical frame a function of the virtual address. The allocator places each page where a hash says it belongs; the MMU replays the same hash to guess the frame and starts fetching the data while the page walk is still running. The walk stays the arbiter — it just no longer sits on the critical path.

class Revelator engine key revelator allocator revelator Kanellopoulos et al., ISCA '26
1. page fault — placement page fault VA hash CityHash64 % N h1 h2 h3 physical memory frame = h(VA), not wherever was convenient 2. later access — the guess races the walk TLB miss VA page-table walk → true frame replay the hash → k candidates prefetch guess into L2 guess = true? hit / discard the guess races the walk — the walk always decides
Placement is a hash of the virtual address, so the frame can be recomputed on demand and validated against the walk.
1 / 5

Orientation

Where the code lives

Revelator is two halves that have to agree: an allocator inside MimicOS and a speculation engine inside the MMU. Everything else — the page walk, the caches, the TLBs — is stock Virtuoso.

FileRole
spec_engine_designs/revelator.{h,cc}the engine — predict() and invokeSpecEngine()
physical_memory_allocators/revelator.hthe allocator — allocate() places a page at its hash
policies/revelator_policy.hthe Sniper-side policy: when to swap a victim out to make room
exception_handling/revelator_exception_handler.{h,cc}routes page faults through the hash-based placement path
spec_engine_designs/spec_engine_factory.hconfig → object; dispatch on "revelator"
mmu_designs/mmu_spec.cccalls the engine after the walk and scores the race
spec_engine_configs/spec_engine_revelator.cfgthe knobs (hashes, predictions, memory size, filters)
address_translation_schemes/revelator.cfgthe assembled scheme: allocator + engine + radix + swap

The mechanism · place → predict → race → validate

The implementation, stage by stage

1

Placement: the allocator obeys the hash

On a page fault the allocator computes k candidate frames from the faulting address and takes the first one that is free. It does not look for a convenient frame — the frame is dictated by the hash, and the index it lands on is the physical page number, shifted past the kernel's reserved region.

Page-table pages hash on address >> 21 rather than >> 12, so a whole 2 MB region's PTEs share one hashed frame — which is what lets the engine speculate on the walk as well as on the data.

2

Prediction: the engine replays the same arithmetic

The engine never consults a table. It re-derives the candidates from the virtual address with the identical hash, adds the kernel offset, shifts back to a byte address and re-attaches the page offset.

Both halves call the same one-liner, so the two sides cannot drift apart by accident:

The invariant. The allocator hashes i · to_hash for i = 1…k; the engine hashes to_hash · (i+1) for i = 0…k−1. Same multiplier set, same order — so candidate j means the same frame on both sides. Change one and you must change the other; there is no cross-check at runtime, the predictions just stop landing.
3

The race: prefetch the guesses while the walk runs

Each candidate is aligned to a cache line and handed to the L2 controller as an MMU prefetch, tagged PAGE_TABLE_DATA or DATA so it lands in the right MSHR map. The engine records the earliest completion, then upgrades that to the specific completion of whichever candidate turns out to be right.

With k hashes the engine issues up to k prefetches, at most one of which is right — so more hashes buy placement freedom at the cost of wasted memory traffic. The optional filters exist to spend that traffic more carefully: filter drops candidate j>0 when hash j is barely used, or when the modelled DRAM latency for that address already exceeds a per-hash threshold.

4

Validation: the walk is still the authority

The MMU calls the engine only after a successful walk, passing the true physical address — speculation never installs a translation, it only warms the cache. A guess that arrives after the walk already finished bought nothing, and is counted as such.

A wrong guess therefore costs bandwidth and cache pressure, never correctness. That asymmetry is why the design can afford to be aggressive when memory is empty.

5

Degradation: what happens when every hash is taken

This is the part that decides whether the idea holds up. If all k candidate slots are occupied and swapping does not free one, the allocator falls back to a linear scan and puts the page somewhere the hash does not point.

That page is now permanently unpredictable: the engine will keep proposing its k hashed frames and keep missing. The fraction of such pages grows with occupancy — which is why the artifact sweeps target_fragmentation from an empty pool to 80% full rather than reporting a single number. More hashes push the fallback further out; swap mode buys room by evicting a victim instead of giving up on the hash.

Read the stats, not just the IPC. hits_per_hash[i] against prefetches_per_hash[i] tells you how much each additional hash actually earns, and spec_late_mispredict how often a guess was both wrong and late. If hash 2 and 3 show hits near zero, the extra memory traffic is pure cost.

Variants

Four engines, one idea

All four are registered in spec_engine_factory.h and selected with [perf_model/mmu/spec] type. Each has a matching allocator, and the pairing is not optional — see the note below.

revelator

Base

k independent hashes, first free slot wins. The default is 1 hash, 1 prediction. Pairs with the revelator allocator.

revelator_open_addressing

Open addressing

Linear probing instead of independent hashes, so the fallback becomes part of the scheme rather than a cliff. Pairs with revelator_alloc_open.

revelator_thp

Huge-page aware

Hashes at huge-page granularity, so one correct guess covers 2 MB — fewer translations to get right. Pairs with the revelator_thp allocator.

numa_revelator

Multi-node

Adds per-node placement on top of the hash, so the candidate set respects NUMA locality. Pairs with numa_revelator.

The engine checks its partner, and gives up if it is wrong. The base engine's constructor reads the allocator's name and, if it is anything other than revelator or revelator_simple, prints a warning, sets m_disabled and turns every method into a no-op. So “Revelator over ReserveTHP” does not run a degraded Revelator — it runs no Revelator, and the results will look exactly like the baseline. Always confirm the scheme pulled in the matching physical_memory_allocators/revelator*.cfg.

Configuration & wiring

What the config sets

Every knob is read once in the engine constructor from the [perf_model/revelator] section; the allocator reads the same section, which is how the two halves stay consistent. Defaults ship in config/spec_engine_configs/revelator_engine_params.cfg.

KeyDefaultMeaning
number_of_hashes1candidate frames the allocator may use, and the engine will guess
number_of_predictions1how many to actually prefetch (must be ≤ hashes — the engine exits if not)
memory_size131072total memory in MB; with kernel_size it fixes the hash table size
kernel_size32768MB reserved for the kernel — hashed frames start above it
target_fragmentation1.01.0 = empty pool, 0.0 = fully occupied. The sensitivity axis
type00 = data and page-table speculation, 1 = data only, 2 = translation only
oraclefalseprefetch the true address — isolates the rest of the design from hash accuracy
perfect_filteringfalseissue only the correct candidate — an upper bound on filtering
filterfalsedrop low-yield or too-slow candidates before issuing them
hash1_usage_threshold0.7occupancy at which the allocator stops relying on hash 1 alone
enable_aggressive_swapoutsfalseevict a victim to keep a page on its hash instead of falling back

A scheme is assembled by including one allocator config and one engine config. revelator.cfg is the reference wiring: revelator_alloc.cfg + spec_engine_revelator.cfg + a 4-level radix table + swap space, on mmu_spec.

Evaluation

What the artifact measures

The harness on the revelator-artifact-release branch runs four suites against a no-speculation ReserveTHP baseline. Single-core numbers are geometric means of IPC over the workload suite; the 4-core suite uses an equal-work per-core heartbeat rather than the global cycle count, which overshoots.

SuiteCompares againstReads out
revelatorReserveTHP floor, SpecTLB, ASAPgeomean IPC speedup, plus oracle and no-translation ceilings
revelator_thpthe floor, ASAP, and 4 KB Revelatorwhat huge-page granularity is worth
utilsweepthe same floor at every fill levelhow fast the win decays from an empty pool to 80% full
multicoreReserveTHP baseline, 4-core mixesSTP, aggregate IPC, harmonic-mean IPC
No numbers on this page. These suites have not been run to completion in the public artifact, so it deliberately quotes none. Two sanity conditions to apply when you do run them: Revelator must land below its own oracle configuration, and the utilization curve must fall monotonically. A design beating its own upper bound means a broken run, not a good result.
SpOT is not in the comparison. It speculates from physical contiguity, so it needs each trace's VMA layout — and when that is missing the allocator silently falls back to per-page buddy allocation instead of failing, which would report a buddy baseline under SpOT's name. The reasoning, and how to re-enable it, are in the artifact README.