Same names, no conflicts: unrolling, renaming and scheduling

After this lesson you will list every dependence of a loop in the exam's vocabulary, unroll it with fresh registers, schedule it with corrected store offsets, count what the midterm asks you to count, and say exactly what Tomasulo's hardware does with all of it — and what it does not.

Take a loop of five instructions that multiplies every element of an array by a scalar, and rewrite it as fourteen. On a plain single-issue pipeline the five-line version needs 9 cycles per element, the fourteen-line version 3.5 — 2.6 times faster, with no new hardware. Three compiler moves make the difference: copy the body four times, give every copy its own registers, reorder the lines.

The notes print the scheduled fourteen lines on pages 191–192, and one of them stores its result to the wrong address — an element that is not even part of this iteration. The slip is a single number, and the professor's VLIW exercises forbid exactly the move that produces it. Find the line.

The wrong line is caught in section 3; section 4 asks how much of all this the hardware does by itself, at run time.

Two kinds of dependence, and only one of them carries a value

Lesson 7 sorted the ways two instructions collide into three kinds. A true dependence (RAW) carries a value from a producer to a consumer. An anti-dependence (WAR) and an output dependence (WAW) carry nothing: the two instructions merely use the same register name. Where the names come from is the first line of these pages (p. 186): name dependences "are constraints imposed by the limited number of registers visible in the Instruction Set Architecture" — thirty-two floating-point names, reused in every iteration of every loop.

The running example of pages 188–193 walks an array downwards and multiplies each element by the scalar in F2. R1 points at the current element; the notes' walkthrough starts it at 80.

I0  LOOP: LD    F0, 0(R1)      ; F0 ← M[R1]
I1        FMUL  F4, F0, F2     ; F2: the scalar
I2        SD    F4, 0(R1)      ; M[R1] ← F4
I3        SUBI  R1, R1, 8      ; 8 bytes down
I4        BNE   R1, R2, LOOP   ; R2 = 0

Exercise 1 of every paper of 2024–2026 opens with a table for exactly this: one row per instruction (I#, TYPE OF INSTRUCTION) and a column ANALYSIS OF DEPENDENCES listing 1. its true data dependences ("with I# for Fx"), 2. its anti-dependences, 3. its output dependences — inside one iteration. For this loop:

The papers' dependence table for this loop: one iteration.
I#TYPE OF INSTRUCTIONANALYSIS OF DEPENDENCES
I0LOOP: LD F0, 0(R1)None
I1FMUL F4, F0, F2True data dependence with I0 for F0
I2SD F4, 0(R1)True data dependence with I1 for F4
I3SUBI R1, R1, 8Anti-dependence with I0 and I2 for R1
I4BNE R1, R2, LOOPTrue data dependence with I3 for R1

What the exam table leaves out is the part this lesson lives on: the dependences between iterations. The next iteration writes F0 and F4 again and reads the R1 that this iteration's SUBI produced. A dependence that crosses iterations is loop-carried; page 188 lists part of the inventory, the complete one is below.

What each instruction of iteration i + 1 depends on in iteration i. Only the R1 chain carries a value.
Instruction of iteration i + 1True data dependenceAnti-dependenceOutput dependence
LD F0, 0(R1)with SUBI(i) for R1with FMUL(i) for F0with LD(i) for F0
FMUL F4, F0, F2—with SD(i) for F4with FMUL(i) for F4
SD F4, 0(R1)with SUBI(i) for R1——
SUBI R1, R1, 8with SUBI(i) for R1with BNE(i) for R1with SUBI(i) for R1
BNE R1, R2, LOOP———

Read the two tables together. Inside an iteration one chain of values runs LD → FMUL → SD, plus SUBI → BNE. Between iterations the only value that travels is R1 — one subtraction per element. Everything else that crosses the boundary is a name: F0 written by both loads, F4 by both multiplies, F0 read by this FMUL before the next LD overwrites it, F4 read by this SD before the next FMUL overwrites it. Those four arcs chain iteration i + 1 to iteration i only because both iterations are spelled with the same two registers.

iteration i iteration i + 1 I0 LD F0, 0(R1) I1 FMUL F4, F0, F2 I2 SD F4, 0(R1) I3 SUBI R1, R1, 8 I4 BNE R1, R2, LOOP I0 LD F0, 0(R1) I1 FMUL F4, F0, F2 I2 SD F4, 0(R1) I3 SUBI R1, R1, 8 I4 BNE R1, R2, LOOP F0 F4 R1 R1 WAW F0 WAR F0 WAW F4 WAR F4 i+1 chained to i

Before: both iterations use F0 and F4

iteration i iteration i + 1, renamed I0 LD F0, 0(R1) I1 FMUL F4, F0, F2 I2 SD F4, 0(R1) I3 SUBI R1, R1, 8 I4 BNE R1, R2, LOOP I0 LD F6, 0(R1) I1 FMUL F8, F6, F2 I2 SD F8, 0(R1) I3 SUBI R1, R1, 8 I4 BNE R1, R2, LOOP F0 F4 R1 R1 WAW F0 WAR F0 WAW F4 WAR F4 only data arcs left

After: iteration i + 1 uses F6 and F8

Fig. 1. Two iterations of the loop. Left: the true dependences (orange, right side) and the four name dependences that chain iteration i + 1 to iteration i (left side: WAR dashed, WAW dotted). Right: the second iteration written with F6 and F8 — the four arcs are gone, the data arcs are untouched, and the only thing still linking the iterations is R1.

Register renaming — replacing the architectural register names of a program by a larger set of storage locations, so that two instructions that share only a name no longer share a location. It deletes WAR and WAW dependences and leaves RAW untouched. The notes (p. 186) distinguish static renaming, by the compiler with ISA-visible registers, typically while unrolling a loop, from implicit (dynamic) renaming, by the hardware at run time — in Tomasulo's algorithm, through the tags of the reservation stations.

The exam asks what renaming is for: the June 2026 theory question lists "5. Dynamic Register Renaming" among the techniques whose "main concepts and benefits" you explain. The benefits are the notes' three ticks (p. 187): instructions that write the "same" architectural register can execute out of order, independent loop iterations can run concurrently, false dependences disappear. The cost depends on who renames: fresh ISA registers and longer code for the compiler, tag-matching hardware and bus bandwidth for Tomasulo.

WAR and WAW are name dependences born of a small register file; renaming deletes them and leaves the true dependences — the only ones that carry values — exactly where they were.

Unrolling: pay the overhead once per four elements

The loop spends two of its five instructions, SUBI and BNE, on nothing but getting to the next element. Loop unrolling is the compiler's answer: copy the body u times into one straight-line block, fold the addresses of the copies into the displacements, and keep one index update and one branch for the whole block. The notes unroll by four (p. 190):

LOOP: LD    F0, 0(R1)
      FMUL  F4, F0, F2
      SD    F4, 0(R1)
      LD    F6, -8(R1)        ; copy 2: fresh registers F6, F8
      FMUL  F8, F6, F2
      SD    F8, -8(R1)
      LD    F10, -16(R1)      ; copy 3: F10, F12
      FMUL  F12, F10, F2
      SD    F12, -16(R1)
      LD    F14, -24(R1)      ; copy 4: F14, F16
      FMUL  F16, F14, F2
      SD    F16, -24(R1)
      SUBI  R1, R1, 32        ; four elements of 8 bytes
      BNE   R1, R2, LOOP

Three things to notice. First, the three intermediate SUBI/BNE pairs are gone: fourteen instructions do the work of twenty. Second, every copy got its own registers, and that is not decoration: written with F0 and F4 four times, the copies would carry the four arcs of Fig. 1 between consecutive copies, and no later load could move above an earlier multiply. Unrolling without renaming replicates the name dependences. Third, F2 is only read, so it needs no copy: only the registers a copy writes are renamed, two for each of the three extra copies, six in all.

The notes list the benefits (p. 190): more independent work, the branch and the index update paid once per four elements, several functional units kept busy, a block the compiler can reschedule. The costs mirror them: longer code (fourteen static instructions instead of five, more instruction-cache pressure), a fuller register file, and a remainder loop of the original body when the number of elements is not a multiple of u. With R1 = 80 the array has ten elements, and the listing above would run R1 through 48, 16, −16 … without ever meeting R2 = 0.

Counting: the midterm's loop

The midterm of 5 May 2025 (notes pp. 534–537) turned this into a counting exercise on a loop that adds two arrays element by element; R1 and R2 start at 0, R3 at 400.

LOOP: LD    F2, 0(R1)
      LD    F4, 0(R2)
      FADD  F6, F2, F4
      SD    F6, 0(R1)
      ADDUI R1, R1, 4
      ADDUI R2, R2, 4
      BNE   R1, R3, LOOP

Four of the seven instructions do useful work (two loads, one add, one store); three are loop overhead (two pointer increments, one branch) — the paper of 11 February 2025 (notes p. 567) asks for the same split as "control vs datapath instructions". R1 runs from 0 to 400 in steps of 4: N = 100 iterations, 700 instructions, 100 branches. Unrolled by u, the useful part is copied u times and the overhead stays three, with the increments scaled to 4u (Fig. 2).

useful, 4 per element overhead, 3 u = 1 3 per el. 7 = 4 + 3 u = 2 1.5 per el. 11 = 8 + 3 u = 4 0.75 per el. 19 = 16 + 3
Fig. 2. Instructions per iteration of the midterm loop after unrolling by u. The useful part grows with u, the overhead stays three, so the overhead per element falls 3 → 1.5 → 0.75 — and the useful four per element never shrinks.

In symbols, for any loop the exam invents: N is the trip count, w the useful and o the overhead instructions per iteration, u the unrolling factor and ρ the registers one copy writes.

\[ \text{iterations} = \frac{N}{u} \] \[ \text{instructions per iteration} = u\,w + o \] \[ IC_u = \frac{N}{u}\,(u\,w + o) = N\,w + \frac{N\,o}{u} \] \[ \text{branches} = \frac{N}{u} \] \[ \text{extra registers} = (u - 1)\,\rho \]
\(N\)
iterations of the original loop: (R3 − R1 at entry) / stride = 400 / 4 = 100
\(w,\ o\)
useful and overhead instructions of one iteration: 4 and 3 here; 3 and 2 for the loop of section 1
\(u\)
unrolling factor, assumed to divide N (otherwise N mod u iterations of the original body remain)
\(\rho\)
registers one copy writes, which must be renamed: F2, F4, F6 here, so ρ = 3; F0 and F4 in section 1, so ρ = 2 (F2 there is only read)

The midterm's question "how much is the global instruction count decrease?" has a closed form worth deriving, because its limit says what unrolling can never do.

  1. The original loop executes the body N times:

    \[ IC_1 = N\,(w + o) = 100 \cdot 7 = 700 . \]
  2. Unrolled by u, with u dividing N, there are N/u iterations of u w + o instructions; the useful part stays N w, the overhead is divided by u:

    \[ IC_u = \frac{N}{u}(u\,w + o) = N\,w + \frac{N\,o}{u} \] \[ IC_4 = 400 + \frac{300}{4} = 475 . \]
  3. Subtract: the useful work cancels, only overhead is saved:

    \[ IC_1 - IC_u = N\,o\left(1 - \frac{1}{u}\right) \] \[ IC_1 - IC_4 = 300 \cdot 0.75 = 225 . \]
  4. Divide by the original count:

    \[ \frac{IC_1 - IC_u}{IC_1} = \frac{o\,(1 - 1/u)}{w + o} \] \[ \frac{IC_1 - IC_4}{IC_1} = \frac{2.25}{7} = 32.14\,\% . \]
  5. Let u grow without bound. The factor 1 − 1/u tends to 1, so the saving tends to

    \[ \frac{o}{w + o} = \frac{3}{7} = 42.9\,\% \]

    — the fraction of the original loop that was overhead. Unrolling can approach it and never pass it: the 400 useful instructions are not negotiable.

The table is the model answer, recomputed by the readers from the notes' solution:

Midterm of 5 May 2025, Question 3: the counts for the original loop and for unrolling factors 2 and 4 (recomputed).
Quantityoriginal×2×4
iterations1005025
instructions per iteration71119
overhead instructions per iteration333
instructions executed globally700550475
branches executed globally1005025
extra registers (renamed copies of F2, F4, F6)—3 (F8, F10, F12)9 (F8 … F24)
FP registers live in the body3612
decrease of the instruction count—150 (21.43 %)225 (32.14 %)
overhead instructions per element31.50.75

The last question, "what is the best unrolling factor between 2 and 4?": 4 — overhead per element 0.75 instead of 1.5, 25 branches instead of 50 — provided the twelve live floating-point values fit in the register file and the longer body in the instruction cache; otherwise 2 is safer. The notes get the trade-off right and the mechanism wrong:

Unrolling by the numbers

The midterm loop (N = 100, w = 4, o = 3, ρ = 3) unrolled by a factor u of your choice, on a machine with 32 FP registers. The readouts recount everything the paper asks for; the plot shows the saving against u — the curve is the formula, the dots are the exact counts including a remainder loop when u does not divide 100, the dashed line is the 42.9 % that can never be reached. The insight to take away: unrolling removes overhead and never useful work, and it pays in registers and code size.

  1. Before moving anything: from u = 2 to u = 4 the number of branches halves. Does the number of instructions halve too? Predict the two totals, then move the slider.
    What you should see

    550 → 475: the branches go 50 → 25, the instructions fall by 13.6 %. The 400 useful instructions are the same at every u; only the 300 → 150 → 75 of overhead move.

  2. Set u = 3. What appears in the iterations readout, and why is the exact saving below the curve?
    What you should see

    33 unrolled iterations plus a remainder loop of 1 iteration of the original body, because 100 mod 3 = 1. The exact saving is 28.29 %, the formula says 28.57 %: the leftover element pays its full overhead. The same happens at every u that does not divide 100: 6, 7, 8, 9, 11 and 12.

  3. Slide to u = 10, then to 11 and 12. Does the saving keep climbing towards 42.9 %, and what happens to the registers?
    What you should see

    u = 10: 430 instructions, 38.57 %, and 30 live FP registers — almost the whole file for one loop. u = 11: still 430, because the remainder iteration pays its full overhead, and 33 live registers, one more than the 32 FP registers: the compiler must spill, and those extra loads and stores are not even in the count. u = 12: 436 instructions, 37.71 %, worse than u = 10. The curve flattens; the costs keep growing.

  4. The midterm's last question: which factor is better, 2 or 4? Read the overhead per element at both and decide — then open the answer.
    What you should see

    1.5 at u = 2, 0.75 at u = 4: factor 4, if the twelve live registers and the 19-instruction body are affordable. The notes' answer (p. 537) reaches the same conclusion but hands the register problem to "the hardware's register renaming" — see the red pen above.

Unrolling by u divides the overhead by u and touches nothing else: saving = o(1 − 1/u)/(w + o), never more than o/(w + o); the price is (u − 1)ρ registers, a body about u times longer and a remainder loop.

Scheduling: loads first, multiplies next, stores last

Unrolling bought fewer instructions per element, but hardly any cycles: in the listing's order every copy still waits inside itself — the multiply for its load, the store for its multiply. The notes give no cycle counts here, so the lesson takes the model of Hennessy and Patterson (5th edition, §3.2), where this example comes from (with ADD.D instead of a multiply; the latencies are those of any FP operation): a single-issue, in-order pipeline where a consumer waits a fixed number of stall cycles after its producer.

Latencies of the model: stall cycles that must separate a producer from its consumer (Hennessy and Patterson, Fig. 3.2 and their example). A correctly predicted branch costs nothing; the next LD issues right after it.
ProducerConsumerStall cycles between them
LDFP operation (FMUL)1
LDSD (data)0
FP operationFP operation3
FP operationSD (data)2
SUBIBNE1

On this machine the original loop takes 9 cycles per element: LD, stall, FMUL, stall, stall, SD, SUBI, stall, BNE — in the formula sheet's symbols m = 5 instructions and k = 4 stalls per iteration, \(CPI_{AS} = (m + k)/m = 1.8\). In the notes' unrolled order it takes 27 cycles for four elements, 6.75 per element: the overhead and its stall are paid once, but the twelve stalls inside the copies remain — no renaming shortens a latency. What hides latencies is code scheduling: reorder the instructions so that independent work fills the waiting cycles. The notes' recipe (p. 191): group independent instructions, delay each dependent one just enough to satisfy its true dependence, hide long latencies with useful work. On the unrolled, renamed body it gives the textbook's order — four loads, four multiplies, the stores, and the index update slipped in before the last two stores so that the branch finds R1 ready:

LOOP: LD    F0, 0(R1)
      LD    F6, -8(R1)
      LD    F10, -16(R1)
      LD    F14, -24(R1)
      FMUL  F4, F0, F2
      FMUL  F8, F6, F2
      FMUL  F12, F10, F2
      FMUL  F16, F14, F2
      SD    F4, 0(R1)
      SD    F8, -8(R1)
      SUBI  R1, R1, 32
      SD    F12, 16(R1)       ; was -16(R1): R1 is 32 lower now
      SD    F16, 8(R1)        ; was -24(R1)
      BNE   R1, R2, LOOP

Fourteen instructions, fourteen cycles, no stall (m = 14, k = 0): 3.5 cycles per element, the number of the hook. Three instructions sit between each LD and its FMUL (one stall cycle was needed), at least three between each FMUL and its SD (two were needed), two between SUBI and BNE (one was needed). Now look at the two stores below SUBI: they moved past the instruction that changes their base register, so the fourth element, at R1 − 24 before the update, sits at R1 + 8 after it (Fig. 3).

R1 = 80 32 40 48 56 64 72 80 -24 -16 -8 0 offsets written in the stores

Before SUBI: the fourth element is −24(R1)

R1 = 48 32 40 48 56 64 72 80 +8 +16 +24 +32 -24(R1) would now hit address 24

After SUBI: the same element is +8(R1)

Fig. 3. The same four elements before and after SUBI R1, R1, 32. A store that moves below the update keeps its element only if its displacement grows by 32: −24 becomes +8, −16 becomes +16. Keep −24 and the store lands at address 24, the fourth element of the next iteration — overwritten before that iteration loads it, while 56 is never written.

That is the slip of the hook. The notes' scheduled listing (pp. 191–192) puts the fourth store after the branch, in a delay slot, with its old displacement:

"SUBI and BNEZ are moved right before the branch to reduce branching penalties" (the notes, p. 192). BNEZ is the branch. What moved is SUBI, hoisted above the last stores so that the branch, which needs the new R1, does not stall on it — one instruction between them is enough here.

Unroll, rename, schedule — in any order

The loop of section 1 on the single-issue pipeline with the latencies of the table above. Three switches, each a move the compiler can make; the listing shows the resulting order cycle by cycle, with every stall and, in the "why" column, the dependence that caused it or the displacement that had to change. The scheduler is a list scheduler: each cycle it issues the ready instruction with the longest path to the end of the iteration; then it puts SUBI into the last stall slot it finds or, if there is none, above the last two stores, as the textbook does, and BNE last. Watch the cycles per element: 3.5 appears only when all three moves act together — unrolling alone leaves the stalls inside every copy, renaming alone changes nothing, and the scheduler needs both.

  1. Before touching anything: 9 cycles per element, 4 of them stalls. Switch on only "unroll ×4". Predict the cycles per element — 9, 6.75 or 3.5?
    What you should see

    6.75: 27 cycles for four elements. The overhead — SUBI, its stall and BNE, three cycles — is paid once instead of four times, but the twelve stalls inside the copies are all still there. Unrolling changes the instruction count, not the latencies.

  2. Keep unrolling on and switch on "rename". Predict how many cycles renaming saves.
    What you should see

    None: 27 cycles again, the same listing with other register names. In program order none of these name dependences stalls this pipeline — every reader of F0 or F4 reads it before the next writer writes it, and the two writers of a register take the same time — so renaming is not a speed-up by itself. It is permission: it is what lets the scheduler move the next load above the previous multiply.

  3. Switch "rename" off and "schedule" on. Why is the result not 14 cycles?
    What you should see

    19 cycles, 4.75 per element and 5 stalls. The loads of copies 2, 3 and 4 cannot be hoisted above the multiply of the previous copy — it still has to read F0 (WAR) — and each multiply cannot pass the previous store, which still has to read F4. The "why" column names the blocking name dependence on every such line: the four copies stay a chain.

  4. Now all three. Count the stalls, and read the displacements of the stores below SUBI.
    What you should see

    14 cycles, zero stalls, 3.5 per element — the order of the listing above. The two stores between SUBI and BNE carry 16(R1) and 8(R1), the correction the notes' line 14 lacks. Try "schedule" alone as well: 7 cycles, with SUBI in a former stall slot and the single store below it turned from 0(R1) into 8(R1).

The notes park the last store in a branch delay slot, a MIPS habit of the slides the example comes from. RISC-V, the ISA of this course, has no delay slot: put the store before the branch — still 14 cycles. The papers' VLIW machine does count one delay slot per iteration: lesson 21 unrolls the same loop for it.

Stalls are latencies, and only reordering hides them: unrolling supplies independent work, renaming makes it movable, scheduling moves it — loads first, FP operations next, stores last, and every access below the index update gets its displacement corrected.

What Tomasulo does with all this: Table 20

Page 193 asks how much of the three moves a dynamically scheduled processor makes by itself, at run time, on the unchanged five-instruction loop. The answer is two of the three, through three mechanisms.

First, implicit renaming through the reservation stations. At issue, an operand that is ready is copied into the station as a value; one that is not becomes the tag of the station or buffer that will produce it, and the register file records for each register only the tag of its latest writer. Two iterations that both write F0 are two different tags — Load1 and Load2 — so the WAR and WAW arcs of Fig. 1 never form. Second, each instruction gets its own reservation station: the LD, FMUL and SD of the next iteration enter fresh stations while those of the current one still wait, so several iterations are active at once without copying the code — the notes' dynamic loop unrolling effect. Third, several functional units and branch prediction: the instances need units to execute on, and issue must go past the BNE before it is resolved, otherwise the next iteration waits for the branch. The notes' sentence for it: "instruction issuing moves faster than control flow".

registers tag stations (tags) waits for F0staleLoad2 F2ready— F4staleMult2 tag = latest writer Load1LD F0, addr 80— Mult1FMUL F4, F0, F2Load1 Store1SD F4, addr 80Mult1 iteration 1 Load2LD F0, addr 72— Mult2FMUL F4, F0, F2Load2 Store2SD F4, addr 72Mult2 iteration 2
Fig. 4. The notes' walkthrough at cycle 8 (lesson 16): two iterations in flight, both spelled with F0 and F4. The register file holds no value for them, only tags — F0 will be written by Load2, F4 by Mult2 — and every station names its producer by tag: Mult1 waits for Load1, Store2 for Mult2. The stations are a larger, invisible register file. Nothing was unrolled.

The notes sum it up in Table 20 (p. 192), the cleanest answer to "compare static and dynamic approaches" you can write at the exam:

Table 20 of the notes: does Tomasulo's algorithm adopt the three compiler techniques?
TechniqueHow Tomasulo handles it
Register renamingYes, dynamically, through the reservation stations and their tags
Code schedulingYes, dynamically, in the order in which operands become available
Loop unrollingNo — but it overlaps iterations of the unchanged code, thanks to renaming and scheduling

Why "no"? Unrolling transforms the program text, and a processor executes the text it is given, issuing each instruction of each iteration once. What the hardware reproduces is the effect — with fresh tags per instance, several iterations proceed at once. What it cannot reproduce is the other half of the gain: every SUBI and BNE is still fetched, issued and executed, five instructions per element instead of 3.5. The open question likes to hear both sides:

Needs the latencies at compile time, (u − 1)ρ extra ISA registers and a remainder loop for awkward trip counts. Costs code size and register pressure. Gains a fixed, stall-free schedule with no extra hardware — 3.5 cycles per element here, and the only way on a VLIW. Fails when a latency is not known in advance (a cache miss).

Needs stations, tag-matching logic, a common data bus and a branch predictor. Renames with tags invisible to the ISA — the 360/91 had four FP registers — on unchanged binaries, and adapts to the latencies that actually happen: a miss simply lets the other iteration run. Costs hardware and bus bandwidth, is limited by the number of stations, and never removes an overhead instruction.

One word of the third mechanism needs care, because the June 2026 question keeps "1. Dynamic Instruction Scheduling", "2. Dynamic branch prediction and speculative execution" and "7. Reorder Buffer" as separate items:

The hook's three moves are the compiler's; the hardware does two of them live and leaves the third to the program text — which is why the exam asks both: the midterm counts an unrolled loop, the papers' Tomasulo table runs one that was never unrolled.

Tomasulo renames with tags and schedules by operand availability, at run time and on unchanged code; it never unrolls — it overlaps iterations, which needs a station per instance, several units and a predicted branch, and without a reorder buffer it can predict but not undo.

Exam-style questions

(Two sub-points in the form of the June 2026 Question 1, which had eight of them for 8 points.) Explain the main concepts and benefits of each one of the following techniques, five lines each: (a) dynamic register renaming in a Tomasulo-style processor; (b) loop unrolling with static register renaming by the compiler. Say for each what it costs and which hazards it removes.

Show model answer

(a) WAR and WAW are name dependences, caused only by reusing the few ISA register names. At issue, the reservation station that will produce a result becomes its name (tag): the register status records the tag, consumers keep either the value (Vj, Vk) or the producer's tag (Qj, Qk), and results are broadcast on the CDB as (tag, value). Benefits: WAR/WAW never stall, more instructions in flight, loop iterations overlap on unchanged code. Costs: tag comparators in every station, CDB bandwidth, limited by the number of stations. RAW remains.

(b) The compiler copies the loop body u times, folds the addresses into the displacements, keeps one index update and one branch, and gives each copy fresh ISA registers ((u − 1)ρ more), so the copies share no names. Benefits: the overhead is paid once per u elements (w + o/u instructions per element instead of w + o) and a large block of independent instructions can be scheduled — loads first, FP operations next, stores last. Costs: code size, register pressure, a remainder loop when the trip count is not a multiple of u; the latencies must be known at compile time. RAW inside each copy remains.

The examiner checks that the two renamings are kept apart — tags invisible to the ISA versus real register names — and that each technique is credited with removing WAR/WAW only, never RAW. A benefit without its cost is half an answer.

(Midterm of 5 May 2025, Question 3, as reproduced in the notes p. 534.) Let's consider the following loop code where registers R1 and R2 are initialized to 0 and R3 is initialized to 400:

LOOP: LD    F2, 0(R1)
      LD    F4, 0(R2)
      FADD  F6, F2, F4
      SD    F6, 0(R1)
      ADDUI R1, R1, 4
      ADDUI R2, R2, 4
      BNE   R1, R3, LOOP

How many iterations of the loop? How many instructions per iteration? How many instructions due to the loop overhead per iteration? How many instructions are executed globally? How many branch instructions are executed globally? Let's consider a loop unrolling version of the code with unrolling factor 2, and then with unrolling factor 4: answer the same questions, and in addition: how many more registers are needed due to register renaming? What are the registers that need to be renamed? How much is the global instruction count decrease? What is the best unrolling factor between 2 and 4? Explain why.

Show model answer

Original loop: R1 goes 0, 4, …, 396 before the branch falls through at 400, so 400/4 = 100 iterations; 7 instructions per iteration; 3 of them overhead (ADDUI R1, ADDUI R2, BNE), 4 useful; 700 instructions and 100 branches executed.

Unrolling factor 2 — the body with the second copy renamed:

LOOP: LD    F2, 0(R1)
      LD    F4, 0(R2)
      FADD  F6, F2, F4
      SD    F6, 0(R1)
      LD    F8, 4(R1)
      LD    F10, 4(R2)
      FADD  F12, F8, F10
      SD    F12, 4(R1)
      ADDUI R1, R1, 8
      ADDUI R2, R2, 8
      BNE   R1, R3, LOOP

50 iterations (100 is a multiple of 2, no remainder loop); 11 instructions per iteration; overhead still 3; 550 instructions and 50 branches executed. Registers: with the same names, the second copy could not write F2, F4, F6 before the first copy had read them (WAR/WAW), so the copies would stay serialised; F2, F4, F6 are renamed to F8, F10, F12: 3 more registers, (u − 1) × 3. Decrease: (700 − 550)/700 = 21.43 %.

Unrolling factor 4: copies 3 and 4 use F14/F16/F18 and F20/F22/F24 with displacements 8 and 12; ADDUI R1, R1, 16 and ADDUI R2, R2, 16. 25 iterations; 19 instructions per iteration; overhead 3; 475 instructions and 25 branches executed; (4 − 1) × 3 = 9 more registers, F8 … F24 — the same three registers F2, F4, F6 renamed three times. Decrease: (700 − 475)/700 = 32.14 %.

Best factor: 4. Iterations and branches halve again (50 → 25) and the overhead per useful add falls from 1.5 to 0.75 instructions. The price is 12 live FP registers instead of 6 and a 19-instruction body; factor 4 is preferable if the architectural register file holds the 12 live values without spilling and the code size is acceptable, otherwise factor 2 is the safer choice.

Write the unrolled code: the displacements (0, 4 / 0, 4, 8, 12) and the scaled increments (8 / 16) are where points are lost. State explicitly that 100 is a multiple of 2 and of 4, so no remainder loop is needed. The renaming here is the compiler's, with ISA registers — do not write that "the hardware" renames them.

The loop of this lesson, I0 LD F0, 0(R1); I1 FMUL F4, F0, F2; I2 SD F4, 0(R1); I3 SUBI R1, R1, 8; I4 BNE R1, R2, LOOP, runs with R1 initialized to 80 and R2 to 0. (a) How many iterations, instructions per iteration, overhead instructions per iteration, instructions and branches executed globally? (b) Write the unrolled version of the loop with unrolling factor 2 by using register renaming. (c) For the unrolled version: iterations, instructions per iteration, overhead, global instructions and branches, how many more registers and which ones, and the decrease of the global instruction count.

Show model answer

(a) R1 takes the values 80, 72, …, 8 at the top of the body: 80/8 = 10 iterations; 5 instructions per iteration, 2 of them overhead (SUBI, BNE); 50 instructions and 10 branches executed.

(b)

LOOP: LD    F0, 0(R1)
      FMUL  F4, F0, F2
      SD    F4, 0(R1)
      LD    F6, -8(R1)
      FMUL  F8, F6, F2
      SD    F8, -8(R1)
      SUBI  R1, R1, 16
      BNE   R1, R2, LOOP

(c) 5 iterations; 8 instructions per iteration; overhead still 2; 40 instructions and 5 branches executed. 2 more registers, (2 − 1) × 2: F0 → F6 and F4 → F8 in the second copy; F2 is only read and is not renamed. Decrease: (50 − 40)/50 = 20 %.

Two traps: forgetting that F2 needs no fresh name (it is never written), and leaving the second copy's displacements at 0 instead of −8. The SUBI immediate doubles; the branch is unchanged. Part (b) is a real exam request: the paper of 11 February 2025 asked to "Write the unrolled version of the loop with unrolling factor 2 by using Register Renaming" (notes p. 568) before running it through the reorder buffer of lesson 18.

Same loop, R1 = 80 (10 iterations). It is unrolled by a factor of 4; since 10 is not a multiple of 4, the leftover elements are handled by a remainder loop of the original body. How many instructions are executed in total?

Show model answer

10 = 2 × 4 + 2: two iterations of the unrolled body (3 × 4 + 2 = 14 instructions each) and two iterations of the original body (5 each): 2 × 14 + 2 × 5 = 38 instructions, with 2 + 2 = 4 branches; the decrease is (50 − 38)/50 = 24 %, less than the 30 % the formula o(1 − 1/u)/(w + o) promises for u = 4, because the remainder pays full overhead.

The examiner wants the split 8 + 2 elements written down; an answer of 35 (10/4 × 14 with a fractional iteration) shows the remainder loop was not understood.

(Single answer.) In the scheduled, unrolled-by-4 loop, SUBI R1, R1, 32 is moved above the store of the fourth element, originally SD F16, -24(R1). After the move the store must read:

  • SD F16, -24(R1) R1 has already been decreased by 32: this writes to R1 − 56 of the old base, an element of a later iteration.
  • SD F16, 8(R1) The old −24 relative to the old R1 is −24 + 32 = +8 relative to the new one: the same address.
  • SD F16, -56(R1) The correction goes the other way: the base got smaller, so the displacement must grow.
  • SD F16, 0(R1) 0(R1) after the update is the first element of the next block, not the fourth of this one.
Show model answer

SD F16, 8(R1): displacement = old displacement − (change of R1) = −24 − (−32) = 8.

This is the line of the notes' page 192 that the hook asked you to find. Whenever a memory access crosses the index update in a schedule, correct its displacement by the update's immediate.

(15 July 2024, quiz 1, single answer — also in lesson 7; it is the vocabulary of this lesson.) Let's consider the following loop: for (i=1; i<=100; i++) { X[i] = X[i] + X[i-1]; /*S1*/ Z[i] = X[i] + Y[i-1] /*S2*/ }. How many loop-carried dependencies are in the code?

  • Answer 1: Only one in S2 because Z[i] depends on Y[i-1] Y is never written in the loop, so Y[i-1] is an input, not a value produced by an earlier iteration.
  • Answer 2: Only one in S1 because X[i] depends on X[i-1] S1 reads X[i-1], which S1 itself wrote in the previous iteration: a true dependence carried from iteration i − 1 to iteration i.
  • Answer 3: Both of them S2's dependence on X[i] is on the same iteration's S1, not on a previous iteration.
Show model answer

Answer 2. The loop-carried dependence is S1 → S1 through X: iteration i reads X[i−1], produced by iteration i − 1. S2 depends on S1 within the iteration, and Y is read only.

Compare with the loop of this lesson: its only loop-carried true dependence is the index R1, which is why its iterations can be overlapped — a loop like S1 could not be, whatever the renaming.

(True/False, motivate your answer.) "Tomasulo's algorithm performs loop unrolling dynamically: the hardware replicates the loop body in the reservation stations so that several iterations execute together."

  • True Nothing is replicated: every instruction of every iteration is issued once, from the instruction stream, into a station of its own.
  • False Tomasulo overlaps iterations of the unchanged code: fresh stations and tags per instance give the effect of unrolling without copying a single instruction (Table 20).
Show model answer

False. Loop unrolling is a compiler transformation of the program text. Tomasulo issues the instructions of each iteration as they arrive, in order, each into its own reservation station with its own tag; because operands are tracked by tag and not by register name, the LD/FMUL/SD of iteration i + 1 can execute while those of iteration i are still waiting — the "dynamic loop unrolling effect" — but the code is never modified or replicated.

The motivation must name the mechanism — stations and tags per instance — and the word "effect": the examiner wants to see that you know what the hardware does and does not do.

Check yourself

The loop is unrolled by 4 without renaming: every copy writes F0 and F4. Which dependences chain copy 2 to copy 1? Select all that apply.

  • WAR on F0: the LD of copy 2 overwrites the F0 that the FMUL of copy 1 still has to read Yes — this is the arc that stops the scheduler from hoisting the second load.
  • WAW on F4: both FMULs write F4 and must finish in order Yes — an output dependence between the copies.
  • RAW on F0: the FMUL of copy 2 reads the F0 loaded by copy 1 No: copy 2's FMUL reads copy 2's own load; no value flows from one copy to another.
  • RAW on R1 through the SUBI between the copies No: after unrolling there is one SUBI, at the end; the copies address their elements through displacements.

Four arcs per boundary, all names: that is what fresh registers delete, and it is why unrolling without renaming leaves the copies a chain.

In the sandbox, renaming the unrolled loop without scheduling it left the cycle count at exactly 27. Why?

  • The stalls are true dependences waiting for latencies; renaming removes only name dependences and nothing was reordered Right: in program order none of these name dependences ever stalls this pipeline, so changing names changes nothing until the order changes.
  • Renaming adds instructions that cost the cycles it saves Renaming adds no instruction; it only changes which registers the existing ones use.
  • The loads became slower because F6 … F16 are farther from the ALU A register name has no distance; every register of the file is read in the same way.

A compiler hoists SUBI R1, R1, 32 above the last two stores of an unrolled loop. What else must change?

  • Nothing: the stores read R1 from the register file and get the right value automatically They get the new value — 32 lower — and with the old displacement they address the wrong element.
  • The displacements of the two stores grow by 32 Correct: the base got 32 smaller, so −24 becomes +8 and −16 becomes +16, and the addresses are unchanged.
  • The displacements of the two stores shrink by 32 That doubles the error: −24 would become −56.

What limits how far a compiler can unroll a loop? Select all that apply.

  • The number of ISA registers: each extra copy needs ρ fresh names Yes — (u − 1)ρ extra registers, real ones, chosen by the compiler.
  • Code size: the body grows about u-fold and must fit the instruction cache Yes — and it is the cost the notes mistake for a benefit (p. 190).
  • A trip count that is not a multiple of u needs a remainder loop Yes — extra code, and the leftover elements pay the full overhead.
  • The number of reservation stations in the processor No: stations limit how many iterations Tomasulo can overlap at run time; the compiler's unrolling does not use them.

Of the three mechanisms of page 193, which one lets the next iteration's LD be issued before the BNE of the current iteration is resolved?

  • Implicit renaming through the reservation stations Renaming removes name conflicts between the iterations; it does not decide whether the next iteration may enter at all.
  • A reservation station for each instruction The free station is where the LD goes — but issue would still wait for the branch without a prediction.
  • Branch prediction Right: the branch is predicted taken, so issue continues past it — "instruction issuing moves faster than control flow". Without a reorder buffer, though, those instructions may not execute before the branch is resolved: nothing could undo their results.

Two iterations of the loop are in flight in Tomasulo's machine, both written with F0 and F4, and no WAW hazard occurs. What makes that possible?

  • Each LD and FMUL instance has its own station, so each write to F0 or F4 is a different tag; the register file keeps only the latest tag Correct: two writers of F0 are Load1 and Load2, and an old broadcast whose tag no longer matches is ignored by the register file.
  • The hardware renamed the second iteration to F6 and F8 before issuing it That is the compiler's static renaming; Tomasulo's tags never appear as register names.
  • The second iteration is issued only after the first has written F4 Then nothing would overlap; the walkthrough of the notes has both iterations fully issued by cycle 8.

Key take-aways

What to watch and read