Same loop, three shapes: unrolling, software pipelining and betting on the hot path

After this lesson you will unroll a loop and count, stall by stall, what it gains; turn a loop into its software-pipelined form and back again; schedule the papers' SP_LOOP on the VLIW table with its four metrics; and explain what trace scheduling, superblocks and predication buy and what each of them costs.

Exercise 2 of the 22 June 2026 paper prints two loops side by side. LOOP1 is the plainest loop there is: LD, FADD, SD, ADDUI, BNE. LOOP2 is made of the same five operations with the first three in reverse order: SD, FADD, LD. On the paper's 3-issue VLIW, scheduled by the same rules, LOOP1 needs 10 cycles per iteration and LOOP2 needs 5.

Read LOOP2 as a program and it looks absurd. It stores F6 before the FADD that computes F6, and the FADD reads F2 before the LD that loads F2. Nothing was added and nothing was removed, yet the loop runs twice as fast. Where did the waiting go — and what does that store actually store?

LOOP1 and LOOP2 are two shapes of one loop. Unrolling, which you met in lesson 15, gives it a third. And when a loop has an if inside, the compiler has one more move: it bets on the side that usually wins.

Unrolling, counted cycle by cycle

A machine that issues several operations per cycle is only as fast as the independent operations it is offered, and one iteration of a short loop offers almost none: every instruction waits for the one before it. The compiler's oldest remedy is loop unrolling: copy the body u times into one straight-line block, fold the addresses of the copies into the displacements, and keep a single index update and a single branch. Lesson 15 counted what that saves in instructions — N/u iterations of u·w + o instructions, (u − 1)·ρ extra registers. Here you count what it buys in cycles, on the notes' own example (p. 285), x[i] = x[i] + s:

LOOP: LD    F0, 0(R1)  ; F0 ← x[i]
      FADD  F4, F0, F2 ; F2 = s
      SD    F4, 0(R1)  ; x[i] ← F4
      SUBI  R1, R1, 8  ; i ← i − 1
      BNE   R1, R2, LOOP

The notes give three numbers for this loop — 10 cycles per iteration, 16 for four iterations once unrolled and renamed, efficiency 87.5 % — but not the machine they were counted on. The first is Hennessy and Patterson's classic count for this very loop (third edition), on a single-issue pipeline whose branch has one delay slot; the other two follow from the same rules:

The delayed-branch latency model of this section: a single-issue, in-order pipeline. Lesson 15 counts the same loop with the same latencies but no delay slot, hence its 9 cycles against 10 here; the papers' VLIW, from section 2 on, has its own numbers.
ProducerConsumerStall cycles between them
LDFADD that uses the loaded value1
FADDSD that stores the result2
SUBIBNE that tests the new index1
BNEthe next instruction1 delay slot, always spent: it holds an independent instruction or nothing
Issue cycle by issue cycle. Column 2: the original loop, one element. Column 3: unrolled four times, renamed and regrouped, four elements — the notes' 16 cycles. Column 4: the same body with the last two stores moved into the stall and the delay slot — 14 cycles.
CycleOriginal×4, regrouped×4, stores moved
C1LD F0, 0(R1)LD F0, 0(R1)LD F0, 0(R1)
C2stallLD F6, -8(R1)LD F6, -8(R1)
C3FADD F4, F0, F2LD F10, -16(R1)LD F10, -16(R1)
C4stallLD F14, -24(R1)LD F14, -24(R1)
C5stallFADD F4, F0, F2FADD F4, F0, F2
C6SD F4, 0(R1)FADD F8, F6, F2FADD F8, F6, F2
C7SUBI R1, R1, 8FADD F12, F10, F2FADD F12, F10, F2
C8stallFADD F16, F14, F2FADD F16, F14, F2
C9BNE R1, R2, LOOPSD F4, 0(R1)SD F4, 0(R1)
C10(delay slot)SD F8, -8(R1)SD F8, -8(R1)
C11SD F12, -16(R1)SUBI R1, R1, 32
C12SD F16, -24(R1)SD F12, 16(R1)
C13SUBI R1, R1, 32BNE R1, R2, LOOP
C14stallSD F16, 8(R1) (delay slot)
C15BNE R1, R2, LOOP
C16(delay slot)

Read the second column first: five instructions, three data stalls, one stall before the branch and an empty delay slot — 10 cycles, half of them wasted. Now copy the body four times in the order of the listing, with F0 and F4 reused. Every copy brings its stalls along: 6 cycles per copy, plus 4 for SUBI, stall, BNE and the slot, makes 28 cycles, 7 per element. Unrolling alone buys little.

What makes the third column possible is renaming. Once each copy has its own registers — F0/F4, F6/F8, F10/F12, F14/F16 — the compiler may regroup: four loads, four adds, four stores. Every consumer now stands four instructions after its producer, more than any latency asks, so the data stalls vanish; left are the stall before BNE and the empty delay slot: 14 instructions in 16 cycles. The fourth column fills both holes with the last two stores, whose displacements grow by 32 because SUBI has already moved R1 (the offset rule of lesson 15): four elements in 14 cycles.

\[ T_{\text{iter}} = \frac{\text{cycles of the unrolled body}}{\text{original iterations it covers}} \] \[ \begin{aligned} \text{Efficiency} &= \frac{\text{useful operations}}{\text{issue slots}} \\ &= \frac{IC}{\#\text{cycles} \times \#\text{issues}} \end{aligned} \]
\(T_{\text{iter}}\)
execution time per original iteration, stalls and empty slots included
\(IC\)
instructions issued by the body; stalls and empty slots are not instructions
\(\#\text{cycles}\)
length of the body's schedule, delay slot included
\(\#\text{issues}\)
issue width: 1 here, 3 or 4 on the VLIW of the papers (it is the code-efficiency line of the example formula sheet)
Equations (66) and (67) on the four versions of the loop. Loop control = SUBI, its stall, BNE and the delay slot when they are not filled with work.
VersionCyclesElements\(T_{\text{iter}}\) (66)Efficiency (67)Loop control per element
original101105/10 = 50 %4
×4, copies in order284714/28 = 50 %1
×4, regrouped164414/16 = 87.5 %1
×4, stores moved1443.514/14 = 100 %0.5

On a 3-issue VLIW the regrouped body pays off more: the four copies are independent of one another, so loads, adds and stores of different copies share bundles — 13 cycles for four elements against 10 for one (section 4). The price is the one lesson 15 named: eight FP registers instead of two, fourteen instructions instead of five (a bigger instruction-cache footprint), and a remainder loop when the trip count is not a multiple of 4.

Unrolling buys independent operations; renaming is what lets the scheduler use them; registers and code size pay for both.

Software pipelining: one body, three elements in flight

Unrolling makes the body bigger so that the scheduler finds independent work inside it. Software pipelining finds independent work without making anything bigger: it takes it from other iterations. Picture three people washing up. In every minute the stacker puts away plate i while the dryer dries plate i + 1 and the washer starts plate i + 2: three plates in progress, one leaving every minute. In the loop, LD washes, FADD dries, SD stacks — and the minute is one pass of the loop body, however many cycles that pass takes.

Software pipelining — reorganising a loop so that each execution of its body holds stages of several consecutive iterations. In Monica Lam's words (1988), "iterations of a loop in the source program are continuously initiated at constant intervals, before the preceding iterations complete". Hennessy and Patterson call it symbolic loop unrolling.

From here on the loop walks upwards, as the papers' loops do — ADDUI R1, R1, 8 instead of SUBI — and the construction takes five steps.

  1. Unroll symbolically. Write iterations i, i + 1 and i + 2 under each other, each one step later than the one above: LD, FADD, SD of iteration i in steps 1–3, of i + 1 in steps 2–4, of i + 2 in steps 3–5.

  2. Look at step 3. It holds the SD of iteration i, the FADD of i + 1 and the LD of i + 2 — one instruction from each, and none waits for another, because each consumes what an earlier step produced. That column becomes the loop body.

  3. Write the body in that order, SD first. Each instruction must read its register before the next one overwrites it: SD reads the F4 that FADD is about to replace, FADD reads the F0 that LD is about to replace. Both are WAR dependences, and on the papers' VLIW a writer may share the cycle of its reader. No register was added: F0 and F4 serve three elements in turn.

  4. Fix the offsets. R1 moves by 8 once per body. The SD works on the element R1 points at, 0(R1); the LD works two elements ahead:

    \[ \text{LD offset} = 2 \times 8 = 16 . \]
  5. Fill and drain. Before the first body, elements 0 and 1 must already be under way: LD x[0], FADD x[0], LD x[1] — the prologue. After the last body, elements n − 2 and n − 1 still owe their last stages: SD x[n−2], FADD x[n−1], SD x[n−1] — the epilogue. The body itself runs n − 2 times, so the exit test compares R1 with R5, the address of x[n−2].

START_UP:
  LD    F0, 0(R1)       ; x[0]
  FADD  F4, F0, F2      ; x[0]
  LD    F0, 8(R1)       ; x[1]
SP_LOOP:
  SD    F4, 0(R1)       ; x[i]
  FADD  F4, F0, F2      ; x[i+1]
  LD    F0, 16(R1)      ; x[i+2]
  ADDUI R1, R1, 8
  BNE   R1, R5, SP_LOOP
FINISH_UP:
  SD    F4, 0(R1)       ; x[n-2]
  FADD  F4, F0, F2      ; x[n-1]
  SD    F4, 8(R1)       ; x[n-1]
prologue loop body × 3 epilogue 1 2 3 4 5 6 7 x[0] LD FADD SD x[1] LD FADD SD x[2] LD FADD SD x[3] LD FADD SD x[4] LD FADD SD one element in, one element out
Fig. 1. The software-pipelined loop over five elements drawn like a pipeline diagram: rows are elements, columns are steps of the program. Each element is loaded, added and stored in three consecutive steps. The highlighted column is one execution of the body — the SD of x[1], the FADD of x[2], the LD of x[3]. Left of the body the prologue fills the line, right of it the epilogue drains it.
Which iteration each instruction of the body works on. The midterm solution in the notes (p. 508) writes the same table with every index one lower: load i + 1, compute i, store i − 1.
Body instructionWorks onAddress
SD F4, 0(R1)x[i], computed in the previous passR1
FADD F4, F0, F2x[i+1], loaded in the previous pass— (registers only)
LD F0, 16(R1)x[i+2]R1 + 2 × 8

How fast is it? Every execution of the body finishes one element — the SD at its top stores a completed result. The cycles between the starts of two consecutive bodies are the initiation interval, II, simply the length of the body's schedule: 5 cycles on the papers' VLIW (section 4), so one element every 5 cycles against 10 for the original loop. Each element now spends three passes between its load and its store, longer than before: only the rate improved, not the time of one element.

\[ \begin{aligned} T(n) &= T_{\text{pro}} + (n - 2)\,II + T_{\text{epi}} \\ &\approx n \cdot II \quad \text{for large } n \end{aligned} \]
\(II\)
initiation interval: cycles of one execution of the body
\(n - 2\)
executions of a three-stage body; with S stages, \(n - S + 1\)
\(T_{\text{pro}},\ T_{\text{epi}}\)
cycles of the prologue and epilogue, paid once per loop

The software-pipelining conveyor

Rows are elements x[0] … x[4], columns are steps of the program, as in Fig. 1. The slider spreads the three stages of every element over S consecutive steps: S = 1 is the original loop, S = 3 the three-stage loop above. Below the drawing, the code that S produces and the schedule of one pass of its body on the papers' 3-issue VLIW (Memory 3, FP 3, Integer 1 or 2 to a branch, one delay slot). The insight to take away: one element completes per execution of the body, so the cycles per element are the body's schedule length — and the body keeps its size and its registers.

  1. Choose the June 2026 loop with S = 1: this is LOOP1, 10 cycles per element. Before you move the slider, predict the schedule length at S = 3, and whether the code needs more FP registers.
    What you should see

    5 cycles: the body is the paper's LOOP2. The registers are still F2 and F6; only the static code grows, from 5 to 11 instructions (3 in the prologue, 3 in the epilogue). One element still finishes per execution of the body, now every 5 cycles instead of every 10: CPI 2.00 → 1.00.

  2. Stay at S = 3 and walk the steps with the buttons. In which steps is x[2] loaded, added and stored? How many elements are in flight during a body step, and how many in the first step?
    What you should see

    x[2] is loaded in step 3, added in step 4, stored in step 5. Every body step works on three elements at once (x[i], x[i+1], x[i+2]); the first step of the prologue on one. That is the line filling up — and the epilogue is the line emptying.

  3. Set S = 2: the load runs one pass ahead, and FADD and SD stay together. Is the schedule halfway between 10 and 5?
    What you should see

    7 cycles. The LD → FADD latency has left the body, but FADD → SD (3 cycles) is still inside it. This two-stage loop is what the notes build on p. 291, with an extra register and a MOV that the reversed body does not need.

  4. Choose the midterm 2025 loop. At S = 3 it is the exam's SP_LOOP: 8 cycles, CPI 0.80, efficiency 41.7 %. No true dependence links its LD, FADD and SD any more — so what limits it? Compare with S = 1.
    What you should see

    The single memory unit (four memory operations need C1–C4) and the integer tail ADDUI → ADD → BNE (1 + 2 cycles) plus the delay slot. At S = 1 — the original loop — it takes 12 cycles, CPI 1.20: software pipelining gives a speed-up of 1.5 in CPI with the same ten instructions in the body.

Unrolling or software pipelining?

The body grows with u: 14 instructions instead of 5 for u = 4.

Removes the loop control of u − 1 iterations: one SUBI and one BNE per four elements.

Needs (u − 1)·ρ more registers: F6 … F16.

The overlap starts and stops inside every unrolled body; a remainder loop is needed when the trip count is not a multiple of u.

The body keeps its size, five instructions, plus a prologue and an epilogue once per loop.

Removes the waiting inside an iteration, not the loop control: one ADDUI and one BNE per element.

Same registers, unless a value must outlive one pass.

The overlap ramps up and down once per loop; the loop must have at least as many elements as stages.

Hennessy and Patterson put it in one line: the two techniques remove different overheads — unrolling the loop-control instructions, software pipelining the time spent below full speed, which an unrolled loop pays at both ends of every unrolled body and a pipelined loop only once. Compilers often do both: unroll a little, then pipeline the unrolled body.

Betting on the hot path: traces, superblocks, predication

Loops are the easy case: their body repeats, so the next iteration is always there to borrow from. Ordinary code is cut into short basic blocks — "only around 4 to 7 instructions are found between branches", the notes say (p. 273) — and a block that short, with its own dependences inside, cannot keep a 3-issue VLIW busy. Global scheduling moves instructions across the branches. Profiling finds the path the program takes most often; the compiler schedules that path as if the branches were not there and pays only when the bet fails.

Trace scheduling (Fisher, 1981) does it in two steps. Trace selection picks the most likely sequence of basic blocks, the trace. Trace compaction then list-schedules the whole trace as if it were one block, moving instructions up and down across the branches inside it — loads first of all, to start their long latency early.

Moving an instruction across a branch makes it run where the original program did not run it, or stop running where it did. Three moves recur, and the sandbox below shows each on a four-block loop:

Side entrances make trace scheduling expensive: every move above one costs a copy on every entering path. A superblock (Hwu et al., 1993) is a trace with a single entry and any number of exits, obtained by tail duplication: the part of the trace below a side entrance is copied, and the off-trace path jumps to the copy. Repairs are then needed only at the exits, outside the superblock; the price is the duplicated tail.

Speculation sandbox: where the repair goes

A loop body with an if inside, in the papers' mnemonics. B1 loads two elements and branches on a condition held in R4 (where R4 comes from does not matter here); B2 adds them (90 % of the passes, the hot path); B3 squares the first (10 %); B4 advances the pointers and closes the loop. The thick blue path is the trace B1 → B2 → B4. Pick what the compiler does: the code is redrawn with the moved instruction on a highlighter stroke and every repair the move needs. Every variant was checked to compute the same results as the original on both paths. Unconditional jumps between the blocks are left out of the counts. The insight: every way of betting on the hot path pays somewhere — in copies, in a duplicated tail, or in issue slots.

  1. Choose "hoist FADD above the branch". Predict before looking: does the cold path B3 need a repair?
    What you should see

    No. On the cold path the FMUL overwrites F4 before anyone reads it, and an add writes no memory, so the hoisted FADD is harmless: no copy, still 11 instructions. The cold path now issues 10 instead of 9 — one wasted add. This is the notes' Example 6, which needed no compensation code either.

  2. Now "sink LD F0 below the branch", then "hoist ADDUI R3 above the join". Where does the copy go in each case, and why on that side?
    What you should see

    Sinking LD F0 into B2 leaves B3 without the F0 it multiplies: a copy goes into B3, on the exit path. Hoisting ADDUI R3 from B4 into B2 means the path B3 → B4 would skip it: a copy goes at the end of B3, on the entering path. Each costs one instruction: 12 in total.

  3. Choose "superblock". How many compensation copies, how many instructions?
    What you should see

    No copy: B3 now continues into its own copy B4′, which still holds ADDUI R3, so the hot region B1 → B2 → B4 has a single entry. But 15 instructions instead of 12: the duplicated tail costs four to save one repair here — and it saves one repair for every move above the join.

  4. Choose "if-convert". How many branches are left inside the loop body, and how many instructions run on the hot path?
    What you should see

    Only the loop branch: the if has become a compare that sets P1 and P2. Ten instructions run on every pass, hot or cold — one more than the branchy hot path, because the FADD and the FMUL are both issued and one of them is thrown away. The cost has moved from code size to issue slots; the next widget shows when that pays.

The notes stop here. Hennessy and Patterson add a fourth tool, which removes the branch instead of scheduling around it — background, not in the notes. A predicated instruction names a one-bit predicate register: it is always issued, but writes its result only when the predicate is true. If-conversion turns the diamond of the sandbox into one block: a compare sets P1 and its complement P2, then (P2) FADD and (P1) FMUL follow without a branch. The control dependence has become a data dependence on a predicate: a bigger block, nothing to mispredict, nothing to compensate. The cost: both arms occupy issue slots on every pass, whichever way the condition goes.

When does if-conversion pay?

A toy model to see the trade-off, not an exam formula. One instruction per cycle. The branchy code issues the branch and one arm; the pipeline always guesses the hot arm and loses P cycles when the cold arm runs. The if-converted code issues one instruction that sets the predicates, then both arms, on every pass. With a hot arm of a instructions taken with probability p and a cold arm of b instructions, the branchy code costs 1 + p·a + (1 − p)·(b + P) cycles per pass and the if-converted code 1 + a + b. The insight: if-conversion wins only for short, balanced arms behind a hard-to-predict branch.

  1. Start with the sandbox's diamond: a = b = 2 and a 6-cycle penalty. At p = 0.9, which version is faster? Below which p does if-conversion win?
    What you should see

    At p = 0.9 the branchy code needs 3.6 cycles, the if-converted 5: keep the branch. If-conversion wins only below p ≈ 0.67 — when the branch is close to a coin toss.

  2. Lengthen both arms to 6. Is there still a p where if-conversion wins?
    What you should see

    No. The if-converted block costs 13 cycles on every pass, the branchy code at most 10 (at p = 0.5). Executing two long arms costs more than an occasional wrong guess.

  3. Make the diamond unbalanced: a = 1, b = 7. And then try a deeper pipeline: back to a = b = 2 with P = 10.
    What you should see

    Unbalanced: never — the hot path would pay for the long cold arm on every pass (9 cycles against at most 8). Deeper pipeline: the break-even moves up to p = 0.8. In this model if-conversion can win at all only when the penalty exceeds the two arms together, P > a + b.

Intel's Itanium put hardware under every idea of this lesson: bundles of three operations with explicit stop bits, predication of almost every instruction, speculative loads, and rotating registers that give a software-pipelined loop fresh register names on every pass. Intel announced the name on 4 October 1999; the nickname "Itanic" appeared the same day. The last Itanium shipments ended on 29 July 2021.

"A loop over 260 elements that does not fit a vector register? Software-pipeline it." The papers ask exactly this (July 2024, quiz 5; June 2026, quiz 3) with trace scheduling and software pipelining, two VLIW compiler techniques, as distractors. A vector processor whose registers hold 64 or 32 elements needs strip mining: strips of at most one register length — lesson 31.

How the papers ask it: LOOP1 against LOOP2, and the midterm's SP_LOOP

Every paper of 2024–2026 has a 5-point VLIW exercise, and two of them schedule a software-pipelined loop. The rules are those of lesson 20: latency 3 for Memory and FP, 1 from the Integer unit to the next Int/FP/L/S instruction and 2 to a branch; one delay slot after the branch, counted; a WAR writer may share the cycle of the last reader of its register ("in the Register File, it is possible to read and write at the same address at the same clock cycle"); no NOPs written. In the solved papers the "critical path for a single iteration" is the schedule length including the delay slot, CPI = cycles / instructions, code efficiency = IC / (cycles × issue width).

The 22 June 2026 paper asks for both loops of the hook, with C1 already filled in, and then for the four metrics of each and the speed-up in CPI:

LOOP1: LD    F2, 0(R1)
       FADD  F6, F2, F2
       SD    F6, 0(R1)
       ADDUI R1, R1, 4
       BNE   R1, R3, LOOP1
LOOP2: SD    F6, 0(R2)
       FADD  F6, F2, F2
       LD    F2, 0(R2)
       ADDUI R2, R2, 4
       BNE   R2, R3, LOOP2
LOOP1 on the 3-issue VLIW (C1 given). The chain LD → FADD → SD waits 3 + 3 cycles; ADDUI shares C7 with SD, the last reader of R1; BNE comes 2 cycles later.
Memory UnitFloating Point UnitInteger Unit
C1LD F2, 0(R1)
C2
C3
C4FADD F6, F2, F2
C5
C6
C7SD F6, 0(R1)ADDUI R1, R1, 4
C8
C9BNE R1, R3, LOOP1
C10(delay slot)
LOOP2 on the same machine (C1 given; C6–C10 stay empty). SD and FADD share C1 (WAR on F6); LD waits for the memory unit and rewrites F2 after FADD read it; ADDUI shares C2 with LD; BNE in C4.
Memory UnitFloating Point UnitInteger Unit
C1SD F6, 0(R2)FADD F6, F2, F2
C2LD F2, 0(R2)ADDUI R2, R2, 4
C3
C4BNE R2, R3, LOOP2
C5(delay slot)
The answer form of the June 2026 paper, filled in.
LOOP1LOOP2
How long is the critical path for a single iteration?10 cycles5 cycles
What performance did you achieve in CPI?10/5 = 25/5 = 1
What performance did you achieve in FP ops per cycles?1/10 = 0.11/5 = 0.2
How much is the code efficiency?5/30 = 0.175/15 = 0.33
How much is the speed up in terms of CPI?2 / 1 = 2
F2 · 3 F6 · 3 R1 · 0 R1 R1 · 2 LD F2, 0(R1) C1 FADD F6, F2, F2 C4 SD F6, 0(R1) C7 ADDUI R1, R1, 4 C7 BNE R1, R3, LOOP1 C9

Before: LOOP1, 10 cycles

F6 · 0 F2 · 0 R2 · 0 R2 R2 · 2 SD F6, 0(R2) C1 FADD F6, F2, F2 C1 LD F2, 0(R2) C2 ADDUI R2, R2, 4 C2 BNE R2, R3, LOOP2 C4

After: LOOP2, 5 cycles

Fig. 2. One pass of each loop as a dependence graph, with the cycle each instruction gets. Left: two true dependences of 3 cycles chain LD, FADD and SD, and the branch waits 2 more after ADDUI — 10 cycles with the delay slot. Right: the same instructions in reverse order are linked only by anti-dependences (dashed), which may share a cycle; LD waits one cycle for the single memory unit, the branch 2 for ADDUI — 5 cycles.

Add the third shape. LOOP1 unrolled by four with renamed registers schedules on the same machine in 13 cycles: four loads in C1–C4, four adds in C4–C7, four stores in C7–C10 with ADDUI in C10, BNE in C12 and the delay slot in C13.

Same loop, three shapes, on the papers' 3-issue VLIW (all three schedules recomputed). LOOP2's 11 instructions are 3 of prologue, 5 of body, 3 of epilogue; ×4 is LOOP1 unrolled by four with renamed registers; efficiency = IC / (cycles × 3).
LOOP1LOOP2×4
static instructions51114
FP registers228
cycles per pass10513
elements per pass114
cycles per element1053.25
code efficiency0.170.330.36

The midterm of 5 May 2025: backwards, then forwards

Exercise 1 of the 2025 midterm (notes pp. 506–512) runs the construction backwards. Part A gives a two-array software-pipelined loop and asks for the original:

START_UP:
  LD    F0, 0(R1)
  LD    F2, 0(R2)
  FADD  F4, F0, F0
  FADD  F6, F2, F2
  LD    F0, 8(R1)
  LD    F2, 8(R2)
SP_LOOP:
  SD    F4, 0(R1)       ; I1
  SD    F6, 0(R2)       ; I2
  FADD  F4, F0, F0      ; I3
  FADD  F6, F2, F2      ; I4
  LD    F0, 16(R1)      ; I5
  LD    F2, 16(R2)      ; I6
  ADDUI R1, R1, 8       ; I7
  ADDUI R2, R2, 8       ; I8
  ADD   R3, R2, R1      ; I9
  BNE   R3, R4, SP_LOOP ; I10

Follow one element. LD F0, 16(R1) loads it; one pass later, when R1 has moved by 8, FADD F4, F0, F0 doubles it; one pass after that, when R1 has moved by 16, SD F4, 0(R1) stores it — at the address it was loaded from. Put the three stages of one element back into one pass, in natural order and with offset 0, and keep the loop control. START_UP and FINISH_UP disappear:

LOOP:  LD    F0, 0(R1)
       LD    F2, 0(R2)
       FADD  F4, F0, F0
       FADD  F6, F2, F2
       SD    F4, 0(R1)
       SD    F6, 0(R2)
       ADDUI R1, R1, 8
       ADDUI R2, R2, 8
       ADD   R3, R2, R1
       BNE   R3, R4, LOOP

Part B schedules SP_LOOP on the 3-issue VLIW. Its only true dependences are in the tail ADDUI → ADD → BNE.

Midterm 2025, Exercise 1.B: SP_LOOP on the 3-issue VLIW (recomputed; it matches the notes' table on p. 511).
Memory UnitFloating Point UnitInteger Unit
C1SD F4, 0(R1)FADD F4, F0, F0
C2SD F6, 0(R2)FADD F6, F2, F2
C3LD F0, 16(R1)ADDUI R1, R1, 8
C4LD F2, 16(R2)ADDUI R2, R2, 8
C5ADD R3, R2, R1
C6
C7BNE R3, R4, SP_LOOP
C8(delay slot)
  1. C1–C2: each SD shares its cycle with the FADD that rewrites its register (WAR: the SD reads the old F4 or F6). The single memory unit pushes SD F6 to C2.
  2. C3–C4: the loads take the memory unit in turn. ADDUI R1 may not precede the last reader of the old R1, LD F0 in C3, so it shares C3 with it; ADDUI R2 shares C4 with LD F2. (The notes say ADDUI starts "when the first SD store finishes": that the store's 3 cycles end in C3 is a coincidence.)
  3. C5: ADD R3, one cycle after ADDUI R2. C6 stays empty because the text gives the Integer ALU a "2 cycle latency to next Branch": the branch reads R3 in ID, so a BNE in C6 would still see the old value. BNE goes to C7, the delay slot to C8. In the exam, give the stated latency as the reason.

Eight cycles: CPI = 8/10 = 0.8, FP ops per cycle = 2/8 = 0.25, code efficiency = 10/(8 × 3) = 0.417. The original loop of Part A, scheduled by the same rules, needs 12 cycles (LD C1 and C2, FADD C4 and C5, SD with ADDUI in C7 and C8, ADD C9, BNE C11, delay slot C12): CPI 1.2, a speed-up of 1.5.

"In our case, there are 8 cycles for each iteration, so the critical path is 8." The notes' solution (p. 512) defines the critical path as "the longest chain of dependent operations that must be executed in sequence because of true data dependencies (RAW)", then answers with the schedule length. 8 is the number the solved papers give, but the true-dependence chain ADDUI → ADD → BNE plus the delay slot is only 5 cycles long; the other 3 come from the single memory unit. Write: "8 cycles, C1–C8 including the delay slot, set by the four memory operations on one memory unit and the tail ADDUI → ADD → BNE".

Back to the puzzle of the hook. LOOP2's SD stores the F6 that the previous pass's FADD computed, and its FADD doubles the F2 that the previous pass's LD loaded: one pass holds three different elements (Fig. 1), linked only by anti-dependences that cost nothing on the papers' machine (Fig. 2). The 3 + 3 cycles of waiting have not disappeared; they now elapse between one pass and the next, while other elements are worked on. What remains inside a pass — SD with FADD, the LD behind them on the single memory unit, the branch 2 cycles after ADDUI, the delay slot — is 5 cycles: one element out of the loop every 5 cycles.

Exam-style questions

Explain the main concepts and benefits of each one of the following static scheduling techniques used by compilers for VLIW processors, and state the main drawback of each: (1) Loop unrolling; (2) Software pipelining; (3) Trace scheduling; (4) Superblock scheduling; (5) Predication (if-conversion).

Show model answer

(1) Loop unrolling replicates the loop body u times in one block, adjusts the displacements of the copies (0, 8, 16, …) and keeps a single index update and a single branch. The compiler renames the registers of each copy, removing the WAR and WAW dependences between copies, so the scheduler can interleave u independent chains and hide latencies. Benefits: fewer branches and index updates per element, fewer stalls, more operations per bundle (x[i] = x[i] + s: 10 cycles per element → 3.5 on a single-issue pipeline with a delay slot). Costs: code size, (u − 1) times the registers a copy writes, a remainder loop when the trip count is not a multiple of u.

(2) Software pipelining reorganises the loop so that each execution of the body contains stages of different iterations — store of i, compute of i + 1, load of i + 2 — in reverse order. The true dependences of an iteration no longer lie inside one body; only anti-dependences and the loop control do. A prologue fills and an epilogue drains the pipeline. Benefits: one iteration completes per execution of the body, with the body's original size and the same registers (June 2026: LOOP1 10 cycles, LOOP2 5 cycles, CPI speed-up 2). Costs: prologue and epilogue code, extra registers when a value must live longer than one pass, at least as many iterations as stages, compiler complexity.

(3) Trace scheduling is global scheduling across basic blocks: profiling selects the most likely path (the trace, acyclic), which is list-scheduled as one block; instructions move across branches (above them speculatively) and across joins. Compensation code — copies of moved instructions on the off-trace exit and entry paths — keeps the other paths correct. Benefits: fills wide instructions beyond the 4 to 7 instructions of a basic block and starts long-latency loads early. Costs: code growth from compensation code, slower off-trace paths, dependence on predictable branches, a complex compiler; stores and instructions that may raise exceptions cannot be speculated freely.

(4) Superblock scheduling restricts the region to a single entry and multiple exits, obtained by tail duplication: the part of the trace below a side entrance is copied for the entering path. Moves then need compensation only at the exits, outside the superblock, so speculation is simpler and safer. Cost: the duplicated code.

(5) Predication removes a branch instead of scheduling around it: a compare sets a predicate register and its complement, and the instructions of each arm are guarded by one of them — always issued, but writing their result only when their predicate is true. The control dependence becomes a data dependence on the predicate, so both arms form one larger basic block that the scheduler can fill, with nothing to mispredict and no compensation code. Cost: both arms take issue slots on every pass, so it pays only for short, balanced, hard-to-predict branches; it needs predicate registers in the ISA (IA-64).

The June 2026 paper asked eight such sub-points for 8 points, about five lines each. A complete sub-point has three things: the mechanism in one or two sentences, the benefit with a number from an exercise, the cost. The contrasts make the answer crisp — unrolling replicates while software pipelining interleaves; trace scheduling repairs at entries and exits, a superblock only at exits, predication nowhere — and so does one sentence saying that the compiler applies all five, the VLIW doing no dependence checking at run time.

Given the following software pipelined loop:

SP_LOOP: SD    F6, 0(R1)
         SD    F8, 0(R2)
         SD    F10, 0(R3)
         FADD  F6, F0, F0
         FADD  F8, F2, F2
         FADD  F10, F4, F4
         LD    F0, 16(R1)
         LD    F2, 16(R2)
         LD    F4, 16(R3)
         ADDUI R1, R1, 8
         ADDUI R2, R2, 8
         ADDUI R3, R3, 8
         BNE   R1, R4, SP_LOOP

Consider a 4-issue VLIW machine with fully pipelined functional units: 2 Memory Units with 3 cycles latency; 1 FP ALU with 3 cycles latency; 1 Integer ALU with 1 cycle latency to next Int/FP and 2 cycle latency to next Branch. The branch is completed with 1 cycle delay slot (branch solved in ID stage). No branch prediction. In the Register File, it is possible to read and write at the same address at the same clock cycle. Considering one iteration of the SP_LOOP, complete the table (Memory Unit 1, Memory Unit 2, Floating Point Unit, Integer Unit; C1 given: SD F6, 0(R1)) by using the list-based scheduling (do NOT introduce any loop transformation), including the branch delay slot. Then: how long is the critical path for a single iteration? What performance did you achieve in CPI and in FP ops per cycle? How much is the code efficiency?

Show model answer

There is no true dependence among the SDs, FADDs and LDs: each SD reads the value its FADD rewrites (WAR), each FADD reads the value its LD rewrites (WAR), and every ADDUI may share the cycle of the last instruction that reads its register. The integer unit has four instructions to issue (three ADDUI and BNE), so each stream's SD and LD are placed together to free its ADDUI as early as possible.

Memory Unit 1Memory Unit 2Floating Point UnitInteger Unit
C1SD F6, 0(R1)LD F0, 16(R1)FADD F6, F0, F0ADDUI R1, R1, 8
C2SD F8, 0(R2)LD F2, 16(R2)FADD F8, F2, F2ADDUI R2, R2, 8
C3SD F10, 0(R3)LD F4, 16(R3)FADD F10, F4, F4ADDUI R3, R3, 8
C4BNE R1, R4, SP_LOOP
C5(delay slot)

Critical path = 5 cycles. CPI = 5/13 ≈ 0.38. FP ops per cycle = 3/5 = 0.6. Code efficiency = 13/(5 × 4) = 0.65.

Five cycles is optimal: six memory operations need three cycles on two units, the ADDUI of the stream loaded last cannot precede C3, and the single integer unit needs a fourth cycle for BNE. A pure program-order tie-break (SD F6 and SD F8 together in C1) is also a list schedule, but it delays LD F2 and LD F4, pushes ADDUI R3 to C4 and BNE to C5: 6 cycles, CPI 0.46, 0.5 FP ops per cycle, efficiency 0.54. Write the reasoning about the integer unit next to the table: it is what justifies pairing each SD and LD in one cycle. Lesson 20 drills the same table as a list-scheduling exercise.

Consider the following software pipelined loop SP_LOOP and its start-up code; R1 points to the first element of an array of n doubles.

START_UP: LD    F0, 0(R1)
          FADD  F4, F0, F2
          LD    F0, 8(R1)
SP_LOOP:  SD    F4, 0(R1)
          FADD  F4, F0, F2
          LD    F0, 16(R1)
          ADDUI R1, R1, 8
          BNE   R1, R5, SP_LOOP

(a) Reconstruct the original (non-pipelined) loop. (b) For each of the first three instructions of SP_LOOP, state which iteration of the original loop it belongs to. (c) Write the finish-up code. (d) For n = 100, how many times is the body of SP_LOOP executed, and which value must R5 hold?

Show model answer

(a) With R3 = initial R1 + 8n:

LOOP: LD    F0, 0(R1)
      FADD  F4, F0, F2
      SD    F4, 0(R1)
      ADDUI R1, R1, 8
      BNE   R1, R3, LOOP

(b) SD F4, 0(R1): iteration i, whose sum was computed in the previous pass. FADD F4, F0, F2: iteration i + 1, whose operand was loaded in the previous pass. LD F0, 16(R1): iteration i + 2 — two strides ahead, 16 = 2 × 8.

(c) FINISH_UP: SD F4, 0(R1) (element n − 2); FADD F4, F0, F2 (element n − 1); SD F4, 8(R1) (element n − 1).

(d) The body handles i = 0 … n − 3: it runs n − 2 = 98 times. R5 = initial R1 + 8 × 98 = R1 + 784, the address of element 98.

The classic mistake is to read SD as "store the result of this pass" and put it first in the reconstructed loop. Follow one value through the passes instead. In (c), remember that the last ADDUI of the body has already moved R1.

Let's consider the two following loops, where LOOP2 is the software-pipelined form of LOOP1 (its load fetches the element two iterations ahead):

LOOP1: LD    F2, 0(R1)
       FADD  F6, F2, F2
       SD    F6, 0(R1)
       ADDUI R1, R1, 4
       BNE   R1, R3, LOOP1
LOOP2: SD    F6, 0(R2)
       FADD  F6, F2, F2
       LD    F2, 8(R2)
       ADDUI R2, R2, 4
       BNE   R2, R3, LOOP2

Both are scheduled by list-based scheduling, including the branch delay slot, on a 4-issue VLIW machine with fully pipelined units: 2 Memory Units with 3 cycles latency, 1 FP ALU with 3 cycles latency, 1 Integer ALU with 1 cycle latency to next Int/FP and 2 cycle latency to next Branch; branch solved in ID with 1 delay slot; read and write at the same register-file address in the same clock cycle. How much is the speed up of LOOP2 over LOOP1 in terms of CPI?

Show model answer

LOOP1 does not change: its chain LD (C1) → FADD (C4) → SD with ADDUI (C7) → BNE (C9) → delay slot (C10) uses one memory unit at a time. 10 cycles, CPI = 10/5 = 2.

LOOP2: SD F6, 0(R2) and LD F2, 8(R2) both fit in C1 (two memory units, two different addresses), with FADD F6 (WAR on F6 and F2) and ADDUI R2 (WAR on R2) in the same cycle; BNE in C3, delay slot in C4. 4 cycles, CPI = 4/5 = 0.8.

\[ \text{speed-up} = \frac{2}{0.8} = 2.5 \]

The second memory unit helps only the software-pipelined loop: LOOP1 is bound by its true-dependence chain, LOOP2 by resources and the branch. The paper itself prints LOOP2's load as LD F2, 0(R2): then the store and the load of one pass touch the same address, the load must not move up beside the store that precedes it, it stays in C2, and LOOP2 takes 5 cycles — a speed-up of 2. With one memory unit, as in the paper, the question never arises.

Let's consider the following code: for (i=0; i<260; i++) Y[i] = X[i] + Y[i]; Which code transformation can be applied to be executed by the VMIPS processor with a Vector Register File composed of 16 Vector registers of 32 elements and 64 bits/element? Motivate your answer.

  • Trace scheduling A VLIW compiler technique for scheduling across branches; this loop has no branch inside and the problem is the vector length.
  • Software pipelining It overlaps iterations of a scalar loop on a statically scheduled machine; it does not make 260 elements fit 32-element registers.
  • Strip mining 260 > 32, so the loop is cut into strips of at most 32 elements, with the vector length register set for each strip.
  • Apply a mask register A mask handles conditional statements inside a vector loop; there is no condition here.
Show model answer

Strip mining. The loop has 260 elements and a vector register holds 32: 260 = 8 × 32 + 4, so a first strip of 4 elements and then eight strips of 32, setting VLR for each strip.

The distractors are the techniques of this lesson; strip mining itself is lesson 31. The motivation point is the arithmetic 260 = 8 × 32 + 4.

True or false: in the steady state, a software-pipelined loop completes one iteration every clock cycle.

  • True That holds only if the whole body, branch included, fits one bundle; LOOP2 of June 2026 completes one iteration every 5 cycles.
  • False One iteration completes per execution of the body, every II cycles, where II is the length of the body's schedule.
Show model answer

False. Each execution of the body finishes one iteration, so the throughput is one iteration per initiation interval — 5 cycles for LOOP2 on the 3-issue VLIW of June 2026, 8 cycles for the midterm's SP_LOOP.

The notes repeat the wrong version five times (pp. 289–291); the CPI you compute in the VLIW exercise contradicts it.

Which of the following statements about trace and superblock scheduling are true? (MULTIPLE ANSWERS)

  • An instruction moved from below a branch to above it needs no compensation code if its result is dead on the other path and it cannot store or raise an exception. Speculation is free exactly in this case: the off-trace path simply never uses the value.
  • An instruction moved above a side entrance into the trace needs a copy on the entering path. Otherwise the path that enters from the side would skip it.
  • A superblock may have several entries but only one exit. The other way round: one entry, any number of exits.
  • Tail duplication removes side entrances at the price of code size. The tail below the entrance is copied for the entering path.
  • A trace may contain a loop's back edge. Traces are acyclic; a back edge ends a trace, which is why loops are unrolled first.
Show model answer

Answers 1, 2 and 4.

The two directions of compensation are what the question checks: copies go on the exit path for moves below a branch and on the entering path for moves above a join.

Check yourself

The notes' loop unrolled four times, renamed and regrouped (four LD, four FADD, four SD, SUBI, BNE) takes 16 cycles on the delayed-branch pipeline. Which lost cycles are left?

  • The stall between SUBI and BNE and the empty delay slot Every data consumer is now four instructions after its producer; only the loop control still wastes two cycles.
  • One LD → FADD stall per copy Each FADD issues four cycles after its LD; the latency asks for a distance of two (one stall), so no stall is left.
  • The WAR hazards between the copies Renaming removed them, and an in-order single-issue pipeline never stalls on WAR anyway.
  • Two FADD → SD stalls in the last copy SD F16 issues at cycle 12, four cycles after its FADD at 8; three would suffice.

In LOOP2 (SD F6, 0(R2); FADD F6, F2, F2; LD F2, 0(R2); …), what does the SD store in a pass?

  • The F6 computed by the FADD of the previous pass The SD reads F6 before the FADD below it overwrites it: a WAR, not a RAW.
  • The F6 computed by the FADD just below it That value is written after the SD has read the register; it is stored in the next pass.
  • Garbage, in every pass Only the very first pass would store garbage without a prologue; that is why the prologue computes a first F6.
  • The F2 loaded by the LD The SD stores F6; F2 is the operand of the FADD.

Why does the three-stage loop load with LD F0, 16(R1) while it stores with SD F4, 0(R1)?

  • The load works for the element two passes ahead, and R1 moves by 8 per pass Stage distance × stride = 2 × 8 = 16.
  • A double is 16 bytes long A double is 8 bytes: that is the stride.
  • The FADD needs two operands, so the load fetches two doubles LD fetches one double; the second operand F2 is the scalar s.
  • The memory unit has a latency of 3, rounded to two elements Latencies are measured in cycles, not in addresses; the offset follows from the stage distance only.

Which of these are costs of software pipelining compared with the original loop?

  • A prologue and an epilogue The pipeline must be filled before the first body and drained after the last.
  • Extra registers when a value must live longer than one pass Then two names are needed for one variable — compilers unroll the body or use rotating registers.
  • A body that grows with the number of stages The body keeps its five instructions; that is the advantage over unrolling.
  • More branches executed per element Still one BNE per execution of the body, that is, per element.

During trace scheduling, an instruction of B1 (above a branch) is moved down into B2, the on-trace successor. The off-trace block B3 also needs its result. What must the compiler do?

  • Put a copy of the instruction in B3 The exit path no longer executes it, so it gets its own copy: compensation code.
  • Nothing: the branch still reads the right values The branch may not care, but B3 uses the result.
  • Put a copy in B4, after the join Too late: B3 reads the value before reaching B4, and the hot path would execute it twice.
  • Undo the instruction in B2 when the branch goes to B3 B2 does not run on that path; there is nothing to undo there.

What does tail duplication buy a trace?

  • A single entry, so moves above the former join need no copies on entering paths — paid with code size The off-trace path enters its own copy of the tail; the trace becomes a superblock.
  • A single exit, so speculation needs no repair at all A superblock keeps all its exits, and moves below a branch still need copies there.
  • A shorter hot path The hot path is unchanged; only the cold path gets new code.
  • It makes profiling unnecessary The hot path still has to be chosen, usually from a profile.

When is if-conversion (predication) likely to beat keeping the branch?

  • Both arms are short Both arms are issued on every pass; short arms make that cheap.
  • The arms have similar lengths With a long cold arm the hot path would pay for it on every pass.
  • The branch is hard to predict, close to 50/50 That is when wrong guesses are frequent and expensive.
  • The branch goes the same way 99 % of the time A strongly biased branch is almost never mispredicted: keep it, or schedule the hot path as a trace.
  • One arm is forty instructions long Forty wasted slots on every pass cost more than an occasional misprediction.

Short, balanced, unpredictable: the three words to write next to if-conversion.

Key take-aways

What to watch and read