2026-08-28
Modern x86 CPUs have two front-end paths feeding the back-end: the legacy decode pipeline (fetch → predecode → decode) and the uop cache (a direct-mapped cache of already-decoded micro-ops). The uop cache delivers up to 6 uops/cycle on Intel Golden Cove; the legacy decoders deliver at most 5 (and only if the instruction mix hits the simple decoders). The switch between these paths is not free — and this transition penalty is one of the most under-appreciated sources of frontend stalls.
When the front-end is streaming uops from the uop cache and hits a region not present, it must:
The observed penalty on Skylake/Ice Lake is typically 2–4 cycles of zero uop delivery, then reduced throughput until the decode pipeline is full. If your hot loop straddles the uop cache boundary — say, 90% of it fits but a helper function called every iteration doesn't — you pay this switch cost twice per iteration.
Real-world example: A ray tracer's inner loop was measured at 3.2 IPC. After inlining a 40-byte SSE normalization helper (pushing the whole loop into the uop cache), IPC jumped to 4.1 — a 28% speedup — with zero algorithmic changes. The perf counter idq.dsb_uops went from 62% of delivered uops to 98%. The uop cache switches disappeared, and so did the front-end bubbles.
Rule of thumb: Intel's uop cache holds roughly 1,500 uops organized as 32 sets × 8 ways × up to 6 uops/line. A rough heuristic: keep hot loops under ~1 KB of x86 code (since typical x86 averages ~4 bytes/instruction and ~1.1 uops/instruction). Every taken branch also ends a uop cache line — so a loop with many small basic blocks fragments its footprint. Diagnose with perf stat -e idq.dsb_uops,idq.mite_uops; if mite_uops exceeds 20% of delivered uops in a hot region, you're paying switch penalties.
Compilers know this: GCC's -falign-loops=32 and Intel's ICX aggressively align loop headers to avoid straddling uop cache line boundaries — because a single misaligned byte can push a whole basic block out of the cache.
