2026-08-21
The Return Address Stack (RAS) is a tiny hardware stack — typically 16 to 32 entries on modern x86 cores — that predicts where every ret instruction will jump. When a call executes, the CPU pushes the return address onto both the software stack (in memory) and the RAS (in silicon). When a ret executes, the CPU pops the RAS and speculatively jumps there, often before the actual return address has been loaded from L1 cache.
But the RAS is a fixed-size circular buffer. When your call depth exceeds its capacity, the oldest entries get silently overwritten. Now here's where it gets nasty: as you unwind the deep recursion, the first N returns predict correctly, but every return past the RAS depth becomes a guaranteed misprediction — the RAS is empty (or worse, wrapped around with stale garbage), so the indirect branch predictor has to fall back to the BTB, which was never trained on returns.
Concrete example: Intel Skylake has a 16-entry RAS. Consider a naive recursive Fibonacci or a tree traversal 20 levels deep:
Rule of thumb: if your recursion depth exceeds 16, expect roughly (depth − 16) × 17 cycles of extra penalty per full unwind on Intel, or (depth − 32) × 17 on AMD Zen (32-entry RAS). A depth-100 recursive traversal costs an extra ~1,400 cycles just in return mispredictions — often more than the actual work.
Worse: setjmp/longjmp, exception unwinding, and coroutine switches desynchronize the RAS from the software stack. After a longjmp that skips 10 frames, the RAS still thinks those frames exist, so the next 10 returns predict garbage. Same with tail-call optimization when it's disabled — every non-tail call in a deep chain fills the RAS. When it's enabled (-O2 with clang/gcc), tail calls become jumps, keeping the RAS shallow.
This is why iterative rewrites of deep recursion sometimes show 2–3× speedups that seem too large to explain — the compiler flattening isn't just saving stack pushes, it's saving the branch predictor from a catastrophe.
