2026-08-23
You already know the uop cache (or DSB, on Intel) skips the front-end by caching pre-decoded micro-ops. What's less obvious is that it has a set-associative structure with brutal packing rules, and violating those rules kicks your loop out even when it easily "fits."
On Skylake through Alder Lake, the uop cache holds ~1,536 uops organized into 32 sets × 8 ways × 6 uops per way. But a "way" isn't just 6 uops — it's a line that must satisfy all of these:
The set index is derived from the linear address of the 32-byte window. So two hot 32-byte windows that hash to the same set fight for those 8 ways — classic conflict misses, but at the instruction level.
Concrete example: A tight vectorized loop with 22 uops that happens to straddle two 32-byte lines fits easily by uop count (22 ≪ 1,536). But if the first line contains 19 uops of dense AVX code, it needs 4 ways — exceeding the 3-way limit — and the entire window is marked uncacheable in the DSB. The loop now runs from the legacy decoders at ~4 uops/cycle instead of the DSB's 6 uops/cycle. That's a 33% front-end throughput cliff from adding one instruction.
Rule of thumb: Keep hot loops under 18 uops per 32-byte code window, and align loop entry points to 32 bytes. If perf stat -e idq.dsb_uops,idq.mite_uops shows MITE (legacy decode) uops climbing on a loop you thought was cached, you've hit the way-packing limit, not the capacity limit.
AMD Zen's op cache uses similar per-window packing (8 ops per entry, up to 8 entries per 64-byte window on Zen 4), so the same pathology appears with different constants. The compiler flag -falign-loops=32 exists specifically to dodge this.
