5.3.12 · D3 · HinglishAdvanced Microarchitecture

Worked examplesReturn address stack

4,199 words19 min read↑ Read in English

5.3.12 · D3 · Hardware › Advanced Microarchitecture › Return address stack

Yeh Return Address Stack (RAS) ka ek worked-examples deep dive hai. Parent note ne machine banai thi: ek chhota sa hardware stack jo har CALL par ek return address push karta hai aur har RET par pop karta hai. Yahan hum us machine ko har us situation se guzarenge jo wo face kar sakti hai aur dekhenge ki har control field ek cycle at a time kya karta hai.

Koi bhi example shuru karne se pehle, un teen numbers ko dobara samjhte hain jo RAS carry karta hai, kyunki neeche har scenario asal mein ek story hai ki ye teen kaise change hote hain.


Scenario matrix

Har woh situation jo RAS face kar sakta hai unhi cells mein se ek hai. Neeche ke examples us cell ke saath tagged hain jo wo cover karte hain, aur mil ke sab ko cover karte hain.

# Cell (case class) Ise kya alag banata hai Covered by
C1 Balanced call/return, no wrap count kabhi tak nahi pahunchta; textbook LIFO Ex 1
C2 Interleaved deep + shallow nesting Multiple pushes phir partial pops phir aur pushes Ex 2
C3 Overflow (push jab count = N) Oldest live entry evict hoti hai; count saturate hoti hai Ex 3
C4 Underflow (pop jab count = 0) Degenerate: no-prediction return karna hoga Ex 4
C5 TOS pointer ka wrap-around TOS seam cross karta hai jab count healthy ho Ex 5
C6 Speculation mis-predict → restore (TOS, count) ka snapshot flush par roll back hota hai Ex 6
C7 Wrong tool (non-return indirect branch) RAS ko fire nahi karna chahiye; degenerate misuse Ex 7
C8 Real-world word problem Ek C call chain ko RAS depth demand mein convert karo Ex 8
C9 Exam twist: accuracy from depth Ek workload ke liye expected hit-rate compute karo Ex 9

Example 1 — Balanced chain, no wrap (cell C1)

Forecast: teen pops reverse order mein → 0x3004, phir 0x2004, phir 0x1004.

Figure 1 neeche same ring ka teen-panel filmstrip hai. Left panel: teeno pushes ke baad, holes 0,1,2 green (live) hain aur 0x1004,0x2004,0x3004 hold kar rahe hain, aur yellow TOS arrow hole 3 par baithe hai (agla free hole), count=3. Middle panel: pehle pop ke baad, hole 2 dim (dead) ho gaya hai aur arrow peeche us par aa gaya hai — popped value 0x3004 wahin se aayi, count=2. Right panel: sab holes dim, arrow wapas hole 0 par, count=0 — ring empty hai. Dekho yellow arrow push par chadhta hai aur pop par utarta hai; dekho green dim hota jaata hai jab count girta hai.

Figure — Return address stack
  1. Push 0x1004 (main ke call ke baad return point). Yeh step kyun? Har CALL record karta hai ki control ko wapas kahan aana hai, jo hai . Ab slot[0]=0x1004, TOS=1, count=1.
  2. Push 0x2004. Kyun? Nested call — ring ko foo ki continuation bhi yaad rakhni hai. slot[1]=0x2004, TOS=2, count=2.
  3. Push 0x3004. Kyun? Sabse gehri call. slot[2]=0x3004, TOS=3, count=3. (Yeh Figure 1 ka left panel hai.)
  4. baz RET → pop. Kyun? LIFO: sabse recent pusher (bar ne baz ko call kiya) pehle answer hota hai. TOS: 3→2, read slot[2]=0x3004, count=2. ✔ predicted 0x3004. (Middle panel.)
  5. bar RET → pop. Kyun? Next-most-recent open call aage answer hota hai. TOS: 2→1, read slot[1]=0x2004, count=1. ✔ 0x2004.
  6. foo RET → pop. Kyun? Sabse purana remaining open call last mein answer hota hai. TOS: 1→0, read slot[0]=0x1004, count=0. ✔ 0x1004. Ring dobara empty (right panel).

Verify: har popped value setup mein print kiye return point ke barabar hai; count exactly par wapas aaya (utne hi pops jitne pushes). Teeno predictions sahi → in returns ke liye zero pipeline flushes.


Example 2 — Interleaved nesting (cell C2)

Forecast: r2, phir r3, phir r1.

  1. push r1slot[0]=r1, TOS=1, count=1. Kyun? Pehli call.
  2. push r2slot[1]=r2, TOS=2, count=2. Kyun? Pehli ke andar nested call.
  3. POPTOS:2→1, read slot[1]=r2, count=1. ✔ r2. Kyun? Inner function pehle return hua.
  4. push r3slot[1]=r3 (freed hole reuse karta hai!), TOS=2, count=2. Yeh step kyun? r2 se khali hua hole ab free real estate hai — ek naya sibling call use reuse karta hai. slot[0] mein r1 untouched hai.
  5. POPTOS:2→1, read slot[1]=r3, count=1. ✔ r3. Kyun? Sabse recent live push r3 thi (sibling call), toh LIFO pehle usse answer karta hai — chahe physically woh same hole use kare jo r2 ne pehle use kiya tha.
  6. POPTOS:1→0, read slot[0]=r1, count=0. ✔ r1. Empty. Kyun? Sirf r1 (original outer call) abhi bhi open hai, toh woh last mein pop hota hai aur ring count=0 par drain ho jaati hai.

Verify: interleaving ne kabhi r1 ko bottom par disturb nahi kiya — woh patiently wait kiya aur last mein pop hua, exactly jaisa LIFO promise karta hai. Reused slot prove karta hai ki ring space recycle karta hai bina corruption ke jab count honor kiya jaata hai.


Example 3 — Overflow (cell C3)

Forecast: last return (A ki taraf) mis-predict karega, kyunki evict ho gaya.

Figure 2 neeche eviction ki teen-panel story hai. Left panel: chaar pushes ke baad ring completely green hai (count = N = 4) aur rA,rB,rC,rD hold kar rahi hai, aur yellow TOS arrow wrap karke wapas hole 0 par aa gaya hai. Middle panel: paanchwa push rE hole 0 mein rA ke upar likhta hai (overwrite flag karne ke liye red dikhaya gaya hai); count 4 par pinned rehta hai. Right panel: sab pop hone ke baad sab holes dim hain aur count=0 hai, aur ek red marker dikhata hai A ka return kuch bhi live nahi paata — mispredict. Teaching point: red hole 0 = sacrifice kiya hua oldest entry.

Figure — Return address stack
  1. push → holes fill hote hain; 4th push ke baad TOS: 3→0 (wraps se), count=4 = . Kyun? Chaar pushes ring bilkul fill kar dete hain; TOS wapas hole 0 par land karta hai. (Figure 2 ka left panel.)
  2. push slot[0] \leftarrow r_E ko overwrite karta hai, phir TOS: 0→1, count = min(4+1,4)=4. Yeh step kyun? Ring full hai. min(count+1,N) rule count cap karta hai jabki TOS aage badhta rehta hai, toh oldest live entry (hole 0 = ) silently sacrifice ho jaati hai. Yeh eviction overflow par intended hai. (Middle panel.)
  3. pop (E ret) → TOS: 1→0 ( se), read slot[0]=r_E, count=3. ✔ . Arrow 0 par wapas kyun jaata hai? Pop always TOS ko ek hole backward step karta hai sabse recently-written live slot tak pahunchne ke liye.
  4. pop (D ret) → TOS: 0→3 ( se wrap), read slot[3]=r_D, count=2. ✔ . Kyun? seam crossing normal ring arithmetic hai; count=3>0 ne slot live prove kiya.
  5. pop (C ret) → TOS: 3→2, read slot[2]=r_C, count=1. ✔ . Kyun? Live entries se neeche step karte rehna.
  6. pop (B ret) → TOS: 2→1, read slot[1]=r_B, count=0. ✔ . Kyun? Last live entry consume hoti hai; count 0 hit karta hai.
  7. pop (A ret) → count already hai, toh TOS move nahi hota. Yeh kyun matter karta hai? Referee block karta hai: A ke liye koi live entry exist nahi karti. Prediction = no-prediction / mis-predict → ek fetch redirect penalty. (Figure 2 ka right panel.)

Verify: exactly ek mis-prediction, aur woh A ka return hai (evicted wala). Chaar sahi, ek galat. Push count 5 > ⇒ overflow guaranteed ek loss.


Example 4 — Underflow (degenerate) (cell C4)

Forecast: "no prediction" — garbage nahi.

  1. Fetch RET, pop attempt. Pehle check kyun? max(count-1, 0) guard aur count==0 test precisely isi moment ke liye exist karte hain.
  2. Count read karo. count = 0. Kyun? Kuch bhi push nahi hua tha.
  3. Pop refuse karo. TOS decremented nahi hota; count se par clamped rehta hai. Output = no valid target. Kyun? Ek badhiya hole par step kar jaata aur jo bhi ancient bits waahan hote woh de deta — ek spurious prediction jo stale data point karta.

Verify: guard ke saath, front end BTB par fallback karta hai ya stall karta hai instead of random address se fetch karne ke. Guard ke bina: (0-1) mod 8 = 7, toh slot[7] read karta — pure garbage. Guard ek galat prediction ko ek honest "I don't know" mein convert karta hai.


Example 5 — TOS ka wrap-around with healthy count (cell C5)

Forecast: w, z, y, x.

  1. push slot[0]=z, TOS: 0→1, count=3. Kyun? Next-write hole 0 hai (jahan arrow point kar raha hai), toh naya return address waahan land karta hai aur arrow aage badhta hai.
  2. push slot[1]=w, TOS: 1→2, count=4. Yeh step kyun? Hole 1 bhi empty tha, toh w last free hole fill karta hai; count tak pahuncha hai bina kisi overwrite ke — har push genuinely empty hole mein gaya, toh yeh full-but-not-overflowing ring hai.
  3. popTOS: 2→1, read slot[1]=w, count=3. ✔ w. Kyun? w sabse recently pushed live entry thi, toh LIFO pehle usse answer karta hai; arrow usse read karne ke liye hole 1 par wapas step karta hai.
  4. popTOS: 1→0, read slot[0]=z, count=2. ✔ z. Kyun? Next-most-recent live push z thi hole 0 mein; arrow ek aur hole peeche step karta hai.
  5. popTOS: 0→3 (seam ke across wrap via ), read slot[3]=y, count=1. ✔ y. Yeh step kyun? Do newest entries chali gayi hain, toh next live wali y hai hole 3 mein — arrow ko hole 0 se wapas hole 3 par roll karke pahuncha, koi special case nahi.
  6. popTOS: 3→2, read slot[2]=x, count=0. ✔ x. Kyun? Sirf x (sabse purani live entry) bachi hai; woh last mein pop hoti hai aur ring count=0 par drain ho jaati hai.

Verify: seam crossing (0 → 3) transparent hai kyunki count (us waqt 2) ne entry live prove ki. Wrap ≠ overflow: yahan count kabhi se zyada nahi hua jabki overwrite kiya, toh koi data loss nahi. Charon sahi.


Example 6 — Speculative mis-predict, phir restore (cell C6)

Forecast: exactly TOS=2, count=2, holes 0,1 = par wapas — jaisa speculation kabhi hua hi nahi.

  1. Snapshot (TOS=2, count=2) speculative branch par. Kyun? Agar branch galat nikla toh hume sab kuch downstream undo karna padega.
  2. Speculative push ( mein) → slot[2]=s, TOS: 2→3, count=3. Kyun trace kiya? Speculation prediction speed ke liye speculative RAS eagerly modify karta hai.
  3. Speculative popTOS: 3→2, read slot[2]=s, count=2. Yeh step kyun? Speculative path mein ek RET thi, aur jab hum guess kar rahe hain to front end ko koi target dena hota hai — toh eager RAS dutifully apni most recent entry () pop karta hai aur usse predict karta hai. Yeh pop utna hi trustworthy hai jitna woh guess jo yahan tak le aaya; agar guess galat tha, toh yeh popped target bhi galat tha.
  4. Flush — dono fields snapshot se restore karo: TOS ← 2, count ← 2. Formally . DONO kyun restore karo? Sirf TOS restore karne se count desync ho jaata; sirf count restore karne se arrow galat hole point karta. Pair (TOS, count) complete stack identity hai.

Verify: post-flush (TOS, count) = (2, 2), snapshot ke identical. slot[2] mein stray s ab count line ke upar hai, toh woh dead hai — guard usse kabhi hand out nahi karega. Speculation ne koi trace nahi chhoda. Isliye RAS checkpointing pointer+counter pair store karta hai, slot contents nahi.


Example 7 — Wrong tool: ek non-return indirect branch (cell C7)

Forecast: double disaster — switch mis-predict hoga aur baad ki real return ke liye RAS corrupt ho jaayegi.

  1. Switch execute hoti hai, galti se pop karta hai. TOS: 1→0, read slot[0]=r, count: 1→0, predicted target = . Yeh galat kyun hai? ek return address hai; switch ko L0 ya L1 chahiye data value x se chosen, jo call/return nesting se kuch bhi lena dena nahi rakhta. RAS yahan galat sawaal ka jawab de raha hai.
  2. True target compute hua → L1. Ab flush kyun? Predicted , toh front end galat path par fetch ho gaya aur use squash karna hoga — ek pipeline flush penalty. Galti yahan tak pahunchi hi kyun? Kyunki RAS ko ek aisa branch ke liye consult kiya gaya jiska target data-dependent hai, stack-structured nahi.
  3. Baad mein real RET aati hai expect karte hue — lekin step 1 ne use already pop kar liya, ab count=0 hai. Doosra penalty kyun? Pop ne legitimate return address pehle consume kar li, toh real RET underflow case (cell C4) hit karti hai: koi live entry nahi → no-prediction/mispredict. Ek misuse se do mispredicts aaye.

Fix: switch/function-pointer/virtual-call targets ko indirect branch predictor ya BTB ko route karo; RAS sirf architectural CALL/RET ke liye reserve karo. Kyun? Woh predictors branch history aur data patterns par key karte hain — data-dependent jump ke liye sahi sawaal — jabki RAS purely LIFO nesting par key karta hai.

Verify: ek misuse se do mispredicts — proof ki RAS ek specialized predictor hai, general nahi. Yeh exactly ek sawaal answer karta hai ("yeh return kahan jaata hai?"), aur kahin aur use karne se misused branch aur ek future legitimate return dono toot jaate hain.


Example 8 — Real-world word problem (cell C8)

Forecast: arrows gino.

  1. Chain ke bottom par har open call list karo. Har ek CALL hai jiska RET abhi fire nahi hua hai. Kyun? Max live count = maximum nesting depth reached, kyunki har unreturned call ring par ek live entry rakhta hai.
  2. Arrows gino: main→server_loop→handle_request→parse_http→route→controller→db_query→malloc→memcpy mein 9 named functions hain aur isliye 8 arrows = 8 open calls. Toh peak count = 8. kyun? functions ki ek linear chain call transitions se judi hoti hai.
  3. se compare karo. Peak demand 8 exactly depth 8 ke barabar hai → ring full hai lekin overflow nahi (count = N, koi eviction nahi); memcpy ka return abhi bhi fit hota hai. Razor's edge matter kyun karta hai? Ek aur nested call (jaise memcpy ek helper call kare) count = N par push karega, main ka return evict karega → outermost return par mispredict.

Verify: 9 named functions ke beech 8 transitions (). ⇒ exactly sufficient, zero margin. Ek safer design choose karta hai (parent ki recommendation) library depth absorb karne ke liye bina eviction ke — "16 entries ≈ 97%" data point se match karta hai.


Example 9 — Exam twist: expected accuracy from depth (cell C9)

  1. ke liye: correct = returns jo ≤8 chahiye = . Yeh do bands sum kyun? ≤4 aur 5–8 dono bands 8 entries ke andar fit hoti hain, toh unke returns kabhi evict nahi hote aur hamesha correctly predict hote hain.
  2. ke liye galat: 9–16 band (8%) aur >16 band (2%) ring se zyada hain → mispredict. Hit-rate . Yeh miss kyun karte hain? Unki required depth se zyada hai, toh outermost entries unki returns aane se pehle evict ho jaati hain.
  3. ke liye: correct = ≤16 bands = ; sirf >16 band (2%) miss karta hai. Hit-rate . Yeh jump kyun? Ring ko 16 tak enlarge karna poora 9–16 band rescue karta hai jo pehle overflow karta tha.

Verify: aur . Yeh parent ke quoted curve par land karte hain (8 entries ≈ 92%, 16 entries ≈ 97%) — close, kyunki hamara clean model speculative aur mismatched-call effects omit karta hai jo real silicon mein couple of points shave karte hain.


Recall Quick self-test

Ek RET pop hoti hai jab count = 0 ho. Ek correct RAS kya output karna chahiye? ::: "No prediction" — count guard pop refuse karta hai aur TOS decremented nahi hota, garbage target se bachata hai. Overflow par (push at count = N), kaun si entry lost hoti hai aur kaun si likhti hai? ::: Sabse purani live entry (current TOS slot par) naye return address se overwrite hoti hai; count par saturate hoti hai. Ek speculative branch flush ke baad, snapshot se kaun se do fields restore hote hain? ::: TOS aur count dono — saath mein woh stack define karte hain; ek akele restore karna ring ko desync karta hai. Ek switch statement RAS use kyun nahi kar sakta? ::: Uske targets data-dependent hain, LIFO-nested nahi; pop karna ek real return address corrupt karta hai aur switch khud bhi mispredict hota hai.

See also: Branch Prediction Basics · Branch Target Buffer · Indirect Branch Prediction · Speculative Execution · Pipeline Hazards · Call Stack · Instruction Fetch