Multicorecache warmthmigrationcontext switchcold misstail latency

Cache Warmth and the Real Cost of Migration

The expensive part of a context switch is not saving registers. It is that the thread resumes on a core whose caches and TLB hold someone else's data, so it must take a burst of cold misses to rebuild a working set that existed perfectly well a moment ago somewhere else.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
Why does a context switch cost far more than saving and restoring registers?
What you wrote
A context switch swaps register state and resumes the thread. Textbook cost: a microsecond or so, mostly kernel bookkeeping.
What the hardware does
Register state is small and cheap. What actually costs is that the incoming thread's data is not in this core's L1, L2 or TLB — it was evicted by whatever ran in between, or it is warm on a different core entirely. The thread then stalls through a burst of misses rebuilding it.
This is why measured context-switch cost varies by orders of magnitude with the workload, why oversubscribed thread pools underperform, and why affinity helps latency. Reasoning about switching as a fixed cost gets all three wrong.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Two costs, very different sizes

The direct cost is saving one register set and loading another, plus scheduler bookkeeping. It is small, roughly fixed, and it is what microbenchmarks measure — which is exactly why microbenchmarks report context switches as cheap. A benchmark that switches between two threads doing nothing has no working set to lose, so it measures the direct cost and nothing else.

The indirect cost is the cold misses the resumed thread takes rebuilding its working set: data in L1 and L2, translations in the TLB, and on a migration possibly a different NUMA node entirely. This cost is not fixed at all. It is proportional to how much state the thread had and how much of it was destroyed, so it ranges from negligible for a thread that touches almost nothing to very large for one with a substantial working set.

The gap between these two is one of the more common measurement traps in systems work, and it is a specific instance of the general problem in Every Way a CPU Microbenchmark Lies: the benchmark removed the thing that was expensive.

Relative cost of the components of a switch. Ratios only. — 1 unit ≈ the direct register save and restoreSIMPLIFIED
Register save and restore×1
Scheduler bookkeeping×1.5
Rebuilding a small L1 working set×20
Rebuilding a large L2 working set×150
Migration: cold caches, cold TLB, possibly remote memory×400
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
Register save and restoreWhat a null microbenchmark measures
Scheduler bookkeepingRun queue manipulation, accounting
Rebuilding a small L1 working setAlready dominates the direct cost
Rebuilding a large L2 working setA thread with real data in flight
Migration: cold caches, cold TLB, possibly remote memoryEverything above plus translation misses and NUMA

Why migration is worse than a plain switch

A switch that resumes a thread on the *same* core may find some of its state surviving — if the intervening thread did not evict everything, part of the working set is still there and the thread warms up quickly. This is why short slices with a small number of threads per core can be relatively cheap.

A migration to a different core has no such luck. The new core's private caches contain nothing relevant, its TLB has none of the thread's translations, and the data is warm somewhere the thread can no longer reach cheaply — worse, some of it may now require coherence transfers from the old core. On a multi-socket machine the memory itself may now be remote, adding the NUMA: Not All Memory Is Equally Far cost on top for the lifetime of the thread's stay.

This is the mechanism that makes Thread Affinity: Pinning and Its Price a tail-latency tool. Migrations are unpredictable, and each one injects a burst of stall cycles into whatever request the thread happened to be serving. The mean barely moves; the tail does.

What survives a switch, by scenario
ScenarioL1/L2 stateTLB entriesTypical impact
Same core, brief interruptionMostly survivesMostly surviveSmall — quick to warm back up
Same core, long slice by another threadLargely evictedLargely evictedModerate — a rebuild burst
Migration to a sibling coreLost — different private cachesLostLarge; some data reachable via coherence
Migration across NUMA nodesLostLostLargest — plus remote memory for the duration

Where it shows up, and what to do

PLATFORM-SPECIFICScheduler migration policy, slice length and how aggressively threads are rebalanced are OS and configuration properties. Container CPU limits interact with this too: throttling produces forced descheduling at period boundaries, with the same cold-restart cost.

Three familiar symptoms trace back to this. Oversubscribed thread pools: more runnable threads than hardware contexts means more switching and more mutual eviction, so throughput falls even though the work is unchanged. Tail latency spikes with no corresponding code path: a request that happened to span a migration pays a stall burst that a request on a quiet core does not. Noisy-neighbour effects in shared environments: another tenant's thread on a sibling logical CPU evicts your working set from the shared L1 and L2, and nothing in your process is responsible.

The controls follow directly and are mostly about reducing switch frequency rather than switch cost: bound runnable threads near the hardware thread count, use affinity for the threads whose tail matters, and prefer batching so that a thread does more work per scheduling opportunity. The last one is underrated — a thread that processes fifty items per wake amortises one warm-up over fifty items instead of paying it per item.

  • Bound runnable threads near the hardware thread count; excess work belongs in a queue, not in more threads.
  • Batch per wake. Amortising one cache warm-up over many items is often a larger win than any micro-optimisation.
  • Pin the latency-critical few to stop unpredictable migrations from landing inside a request.
  • Expect noise in shared environments. A sibling tenant can evict your working set and you cannot prevent it.

Key points

  • A context switch has a small fixed direct cost and a large variable indirect cost from lost cache and TLB state.
  • Microbenchmarks measure the direct cost only, because a thread doing nothing has no working set to lose.
  • Migration to a different core is far worse than a same-core switch: nothing survives, and memory may become remote.
  • This is the mechanism behind oversubscription penalties, migration-induced tail latency, and noisy neighbours.
  • Reducing switch *frequency* — bounded pools, batching, selective affinity — beats trying to make switches cheaper.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Timer or preemption → scheduler: the running thread's registers are saved and another thread is selected.
  2. 2
    Intervening thread → caches: it fills L1, L2 and TLB with its own data, evicting the previous occupant's.
  3. 3
    Resumed thread → cold caches: the original thread restarts and misses on data that was resident moments earlier.
  4. 4
    Miss burst → memory hierarchy: those misses walk L2, LLC and possibly DRAM, stalling the thread repeatedly.
  5. 5
    On migration → different core: private caches and TLB hold nothing relevant, and coherence or NUMA costs may apply.
What people conclude from this — wrongly
  • "A context switch costs about a microsecond" — that is the direct cost; the cache rebuild is usually larger.
  • "The benchmark says switching is cheap, so oversubscription is fine" — the benchmark had no working set to lose.
  • "Latency spikes must be GC or the network" — an unlucky migration produces the same shape and leaves no trace.
  • "My container has a CPU limit, so it just runs proportionally slower" — throttling deschedules, and each restart is cold.

Consequences, controls and cost

What it causes
  • • Context-switch cost that varies by orders of magnitude between workloads and between benchmarks and production.
  • • Oversubscribed pools performing worse than smaller ones on identical work.
  • • Tail latency spikes on requests that happened to span a migration.
  • • Performance variance in shared or containerised environments that no application change explains.
What you can do
  • • Bound runnable threads near the hardware thread count, queueing excess work rather than creating more threads.
  • • Batch work per wake so one warm-up is amortised over many items.
  • • Use affinity for the small set of latency-critical threads to remove unpredictable migrations.
  • • In containers, check whether CPU limits are causing throttled descheduling, which forces the same cold restart.
How to see it
  • • Track voluntary and involuntary context switches separately; involuntary ones indicate preemption from oversubscription.
  • • Track thread migrations per second, not just switches — migrations carry the larger cost.
  • • Correlate latency outliers with migration events to confirm the mechanism rather than assuming it.
  • • Compare cache miss rate immediately after a switch against steady state; the burst is the indirect cost made visible.
What it costs
  • • Bounded pools reduce switching but can underuse hardware if threads block unexpectedly.
  • • Batching amortises warm-up at the cost of increased latency for the first item in each batch.
  • • Affinity removes migrations but reduces the scheduler's ability to balance load.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • PLATFORM-SPECIFICMigration policy, slice length and rebalancing aggressiveness are OS and configuration properties; container CPU quotas add forced descheduling at period boundaries with the same effect.
  • SIMPLIFIEDThe model treats caches as fully lost on migration. In practice some data is reachable via coherence from the previous core, and shared LLC contents may survive — which reduces but does not remove the cost.

Misconceptions

Claim
“Context switches cost a fixed number of microseconds.”
Reality
The direct cost is roughly fixed and small. The indirect cost — rebuilding cache and TLB state — is proportional to the working set destroyed, so total cost varies by orders of magnitude between workloads.
Claim
“Threads are cheap because switching is cheap.”
Reality
Blocked threads are cheap. Runnable threads beyond the hardware thread count force switching *and* mutual cache eviction, so each additional one degrades the others. The cost is in the interference, not the switch.
Claim
“A migration is just a context switch on a different core.”
Reality
It also discards every private cache line and TLB entry the thread had, and on multi-socket machines can make its memory remote for the rest of its stay. Same-core switches often preserve much of the working set; migrations preserve none of it.

Apply it

Where the rest of this lives

Concurrency & Parallelism
Why bounded thread pools outperform unbounded ones

The concurrency argument is about queueing and resource limits. The hardware argument is here: past the hardware thread count, extra runnable threads mostly evict each other's cache state, so they add interference rather than throughput.