The power tax: a busy core that does nothing still pays
Dynamic power in CMOS is P = α·C·V²·f. The term that matters here is
α, the activity factor — the fraction of transistors that toggle each cycle. A core
spinning in while(locked){} toggles its whole pipeline every cycle to re-confirm
a value that hasn't changed. Same energy, zero useful work. The escape is to gate the
clock: stop toggling, and α (and with it dynamic power) collapses to zero, leaving only
static leakage. That is the entire reason WFI/WFE exist.
To your bones: the lock isn't changing while you spin, so every one of those watts buys nothing. Gating the clock is free performance — but a gated core is deaf. We now need a way to wake it. Hold that thought.
The race: why shared data needs a lock at all
Two cores both do counter++. That single line is really
read → add → write — three steps. If the steps interleave, an update is silently lost.
A lock forces the three steps to happen indivisibly. Step through both worlds and watch
the counter.
To your bones: correctness demands that "test the lock and take it" be one atomic, uninterruptible act. The hardware that delivers that atomicity is the exclusive monitor — next.
The exclusive monitor: how silicon makes a read-modify-write atomic
ARM's atomicity is optimistic. LDAXR (load-acquire exclusive)
reads the lock and arms a per-core global monitor for that address (state:
Open → Exclusive). STXR (store-exclusive) only commits if the monitor is still
Exclusive; otherwise it fails and returns 1, and you retry. Crucially, any write to the
watched address clears every other core's monitor (Exclusive → Open). Watch two cores race —
and note the monitor-clear, because it is the seed of the wake-up mechanism later.
To your bones: STXR succeeding is hardware proof that
nobody touched the address since your LDAXR. And the monitor-clear that fails the
loser's store is the same physical event that will later ring a sleeping core's doorbell.
Cache-line bouncing: the naive lock saturates the fabric
Even with atomics, how you spin matters. Test-and-Set hammers the lock with an atomic write every cycle — and an atomic write needs the cache line in a writable (Modified) state, owned by exactly one core. So the line ping-pongs around the ring, each grab invalidating everyone else. Test-and-Test-and-Set spins on a plain load instead; a load only needs a Shared copy, so every core reads from its own L1 in silence. Toggle and watch the interconnect traffic counter.
To your bones: TTAS kills the traffic, but the core is still awake, still toggling its load loop, still paying chapter 1's power tax and stealing SMT issue slots. The last move is to stop spinning entirely — and sleep.
WFI vs WFE: two ways to sleep, two ways to wake
Both park the core in a low-power state. The difference is the wake set. WFI wakes only on an interrupt (it ignores events entirely). WFE wakes on everything WFI does plus a system event, tracked by a one-bit sticky Event Register. And the safety detail that makes lock code race-free: if the Event Register is already set when you execute WFE, it clears the bit and returns immediately — it never sleeps. Press the signals and watch each core.
To your bones: WFI = "wake me when the outside world has work" (the idle loop). WFE = "wake me when a peer signals me" (the lock loop). The sticky register means a wake-up that arrives a nanosecond too early is never lost.
The full WFE spinlock: every layer at once
Now we assemble it. Core A holds the lock and runs its critical section. Cores B
and C tried to acquire, saw it held, and executed WFE — they're asleep at
leakage-only power, monitors armed (Exclusive). When A releases with STLR lock,#0,
that single store clears B's and C's monitors, which generates an event for each —
no explicit SEV needed. They wake, re-run acquire; one wins, the other's
STXR fails and it sleeps again. Step through the whole machine.
// AArch64 — the canonical WFE spinlock acquire: LDAXR w1, [lock] // load-acquire exclusive → arms global monitor CBNZ w1, wait // lock held? go to sleep STXR w2, w0, [lock] // try to take it CBNZ w2, acquire // store-exclusive failed → retry // --- lock owned --- wait: WFE // sleep at leakage-only power until an event B acquire // woken → re-check release: STLR wzr, [lock] // store-release 0 → clears waiters' monitors → wakes them
The two instructions, side by side
| Axis | WFI — Wait For Interrupt | WFE — Wait For Event |
|---|---|---|
| Wakes on | Interrupts, debug, reset | All of WFI's sources + events: SEV / SEVL, the timer event stream, and a monitor Exclusive→Open transition |
| Event Register | Ignored — SEV cannot wake it | Consulted & cleared. If already set, returns immediately (lost-wakeup guard) |
| Canonical use | OS idle loop; deep power-down | Spinlocks; short inter-core waits |
| Power-state depth | Good candidate for retention / power-gate (long waits) | ARM advises shallow states — WFE waits are short and snoop-sensitive |
| Mental model | "Wake me when the world has work" | "Wake me when a peer signals me" |
The whole chain in one breath: physics taxes every toggle (ch.1) → so we gate the clock and need a wake mechanism → shared data needs atomicity (ch.2) → delivered by the exclusive monitor (ch.3) → whose monitor-clear also doubles as a wake event → naive spinning floods the fabric (ch.4) → TTAS quiets it but still burns power → WFE sleeps and is woken precisely by that monitor-clear event (ch.5) → giving the full, near-free spinlock (ch.6). WFI is the same idea pointed at interrupts instead of events.