Instruction-Level Parallelism and Dependences
Imagine a CPU that can fetch, decode, and execute five instructions per cycle. How many instructions per cycle does it actually get? Often far fewer than five. The culprit is rarely the hardware's raw capacity — it is the program itself. Instructions in a single stream are not independent atoms. They read and write shared registers and memory locations, and they branch based on runtime conditions. These relationships, called dependences, cap how much work a processor can overlap. This holds no matter how wide or deep its pipeline becomes. Understanding dependences is the conceptual bridge between simple Pipelining and the aggressive machinery of modern processors. A basic pipeline assumes instructions can be issued one per cycle with occasional stalls. Real programs violate that assumption constantly. Every out-of-order engine, every register-renaming unit, every branch predictor in a commercial CPU exists to work around a specific kind of dependence. You cannot understand why a Cortex-A78 or an Apple M-series core has 600-entry reorder buffers and elaborate speculation. First you must understand exactly what limits are being fought. This article gives a precise taxonomy: data dependences (RAW), and the pseudo-dependences that are artifacts of a finite register file rather than genuine data flow (WAR, WAW), plus control dependences from branches. We connect each dependence type to the pipeline hazard it causes. We show how a scheduler-level algorithm exposes and quantifies available instruction-level parallelism (ILP). We also derive why the trip count of a program's dependence chain — not its instruction count — bounds achievable speedup. By the end, you will be able to inspect a short instruction sequence and classify every hazard it contains. You will be able to compute the critical path length. You will also be able to predict the minimum number of cycles any reasonable in-order or out-of-order machine needs to execute it. This sets up naturally into how Superscalar Execution and Out-of-Order Execution attack these very constraints.
- ILP is the amount of independent work in an instruction stream that can be executed simultaneously or reordered.
- Three fundamental dependence types exist: data (RAW), anti (WAR), output (WAW), plus control dependences from branches.
- RAW is a true dependence reflecting real data flow; WAR and WAW are name dependences, artifacts of register reuse, removable by renaming.
- A hazard is what happens when hardware executes instructions in an order that would violate a dependence if left unhandled.
- The length of the longest dependence chain (the critical path) lower-bounds execution time, regardless of available hardware width.
- Techniques like forwarding, renaming, and speculation don't eliminate dependences — they let hardware satisfy them without stalling.
The maximum speedup from parallel execution of one instruction stream is bounded by its longest true-dependence chain, not by how many functional units the machine has.
The idea
A dependence is a relationship between two instructions that constrains the order in which they may correctly execute. Dependences are properties of the program — they exist whether or not the hardware is pipelined, superscalar, or in-order. A hazard is what happens when the hardware's actual execution order threatens to violate a dependence; hazards are properties of the machine and schedule, and they are what stalls, bypassing logic, or renaming must resolve. This distinction — dependence in the program, hazard in the machine — is the single most important conceptual separation in this topic, and it is frequently blurred in casual usage. There are three categories of dependence between two instructions (earlier) and (later) that touch a common storage location (register or memory address):
- True (data, RAW — read-after-write): reads a value that writes. This reflects genuine data flow: needs 's result. RAW dependences can never be eliminated — they are the actual computation.
- Anti (WAR — write-after-read): writes a location that reads. If 's write happened before 's read, would read the wrong (new) value. This is not about data flowing from to ; it's a name collision — and happen to use the same register or address.
- Output (WAW — write-after-write): both and write the same location. If reordered, whichever write actually lands last determines the final value seen by later readers, so the two writes must resolve in program order (or an equivalent state must be maintained).
WAR and WAW are jointly called name dependences: they exist only because a finite set of register names (or memory addresses) gets reused across a program, not because data actually flows between the two instructions. A machine with an unlimited supply of unique register names (register renaming) can eliminate WAR/WAW hazards entirely, because each write could simply get its own fresh name. RAW dependences admit no such trick — the value must exist before it can be consumed. Separately, a control dependence ties an instruction's execution to the outcome of a preceding branch: instruction is control-dependent on branch if whether executes at all depends on 's direction. Control dependences constrain which instructions may be fetched and committed, and they are resolved (or speculated around) by branch prediction rather than by forwarding or renaming.
How it works
Detecting and reasoning about dependences is mechanical: scan the instruction stream, and for every pair of instructions that share a register or memory operand, classify the relationship by which one reads and which one writes. The corresponding hazard arises only if the hardware's execution timing would let the later instruction's action (read or write) occur before the earlier one's, in a way that produces a result different from strict program order.
Data hazard resolution in an in-order 5-stage pipeline (IF, ID, EX, MEM, WB) has three canonical hardware responses: 1. Stall — hold the dependent instruction in decode until the value is ready. 2. Forward (bypass) — route the result directly from the producing stage's output to the consuming stage's input, skipping the register file. 3. Reorder / rename (in more aggressive machines) — let independent instructions proceed while the dependent one waits, tracked via renaming tables or scoreboards. The following pseudocode formalizes dependence detection — the first step any scheduler, compiler, or hazard-detection unit performs — followed by a simple greedy list scheduler that shows how detected dependences translate into a lower bound on cycles, foreshadowing what out-of-order hardware does dynamically. ```
# Step 1: Build the dependence graph for a basic block of instructions
# Each instruction has: dest register (or none), list of source registers,
# and (optionally) a memory address class for loads/stores. function BUILD_DEPENDENCE_GRAPH(instructions): n = length(instructions) edges = [] # (i, j, type) meaning instr i must precede instr j for i in 0 .. n-1: for j in i+1 .. n-1: I, J = instructions[i], instructions[j]
# RAW: later instr reads what earlier instr writes if I.dest is not None and I.dest in J.sources: edges.append((i, j, "RAW"))
# WAR: later instr writes what earlier instr reads if J.dest is not None and J.dest in I.sources: edges.append((i, j, "WAR"))
# WAW: both write the same destination if I.dest is not None and J.dest is not None and I.dest == J.dest: edges.append((i, j, "WAW"))
# Memory RAW/WAR/WAW use address analysis (may be conservative,
# i.e., assume dependence unless addresses are provably distinct) return edges
# Step 2: Compute the critical path length — this is the true ILP bound.
# Only RAW edges impose real latency; WAR/WAW just impose an ORDERING
# constraint, not a latency constraint (assuming renaming is available). function CRITICAL_PATH_LENGTH(instructions, edges, latency): n = length(instructions) finish = array of size n, initialized to 0 for i in 0 .. n-1: # process in program order
# (a valid topological order for
# a basic block's RAW edges) ready_time = 0 for (p, q, kind) in edges: if q == i and kind == "RAW": ready_time = max(ready_time, finish[p]) finish[i] = ready_time + latency[instructions[i].op] return max(finish) # length of the longest true-dependence chain
# Step 3 (illustrative): greedy scheduler assigning ops to W parallel
# functional units, respecting RAW order, to show achievable speedup. function GREEDY_SCHEDULE(instructions, edges, W, latency): ready_at = array of size n, filled via CRITICAL_PATH_LENGTH logic cycle = 0 scheduled = set() slot_used = {} # cycle -> count of units busy while length(scheduled) < n: candidates = [i for i in 0..n-1 if i not in scheduled and all RAW-predecessors of i are in scheduled and ready_at[i] <= cycle] issue_count = min(W, length(candidates)) for k in 0 .. issue_count-1: mark candidates[k] as scheduled cycle = cycle + 1 return cycle # total cycles used — compare to n/W (ideal) vs
# critical path length (true lower bound) ``` The key move is in `CRITICAL_PATH_LENGTH`: only RAW edges are used to compute `ready_time`. WAR and WAW edges are ordering constraints that a renaming mechanism can satisfy for free (by giving the writer a new physical register), so they don't lengthen the true data-flow critical path — they only matter for machines without renaming, where they'd force additional stalls or serialization.
Complexity & correctness
Detecting dependences for a basic block of instructions by pairwise comparison, as in `BUILD_DEPENDENCE_GRAPH`, costs time in the worst case, since every pair of instructions is inspected once. In practice, register dependences can be found in using a "last writer" and "last readers" table per register (scan once, keep the index of the most recent instruction that wrote or read each register), because a register file has a small, fixed number of names. Memory dependences are harder: determining whether two computed addresses can alias is undecidable in general, so real compilers and hardware use conservative approximations — if it cannot be proven that two addresses differ, a dependence is assumed. This conservatism is correctness-preserving (never misses a real hazard) at the cost of precision (sometimes creates false dependences that block legal reordering).
Computing the critical path length (`CRITICAL_PATH_LENGTH`) is where is the number of RAW edges, since it's a single dynamic-programming pass over a DAG in topological (here, program) order — each edge relaxes one node's finish time exactly once, identical in structure to longest-path-in-a-DAG. Program order is guaranteed to be a valid topological order for the RAW edges within a basic block, because a RAW edge always points from an earlier instruction to a later one by definition (you cannot read a value before some instruction — including possibly a much later one, which would be illegal — writes it; well-formed single-pass programs only reference prior writes).
Correctness argument for the critical-path lower bound. Let be the length (in cycles, summing operation latencies) of the longest chain of RAW-dependent instructions in a program. Claim: no machine, however wide or however cleverly it reorders independent work, can finish the program in fewer than cycles. Proof sketch: each instruction on the critical chain must genuinely wait for its immediate predecessor's result — this is what RAW means — so the finish time of the -th instruction on the chain is at least (finish time of instruction ) + (its own latency). By induction, the last instruction on the chain cannot finish before the sum of all latencies along the chain, i.e., before cycle . Since the program cannot be considered "done" before its last-finishing instruction, total execution time . This holds regardless of issue width, functional-unit count, or scheduling cleverness, because none of those resources can make an instruction execute before the value it needs exists.
Why WAR/WAW don't belong in this bound. A machine with renaming can always give instruction 's write a brand-new physical register distinct from anything instruction reads or writes, making the WAR/WAW ordering requirement trivially satisfiable with zero added latency — the writes/reads just target different physical storage, and a rename table maps architectural names to the correct physical ones for later readers. This is exactly why the pseudocode excludes WAR/WAW from `ready_time`: they constrain which values end up visible to whom, not how long you must wait for a value to be computed.
Tradeoffs. The bound is a lower bound, not automatically achievable — an in-order pipeline with no forwarding, for instance, will do far worse than because of unnecessary stalls, and even an ideal out-of-order machine with infinite resources is bounded below by but also constrained above by finite issue width, finite renaming resources (reorder buffer size), and branch misprediction penalties from control dependences. Memory disambiguation conservatism can also inflate the effective critical path beyond the true one, since false address dependences serialize instructions that are actually independent.
Worked example
Consider this instruction sequence (register-register ALU ops, single-cycle latency 1 except MUL/DIV at 3 and load latency 2): ``` I1: LOAD R1, 0(R2) ; R1 = Mem[R2] I2: ADD R3, R1, R4 ; uses R1 I3: MUL R5, R3, R3 ; uses R3 I4: ADD R6, R7, R8 ; independent of R1..R5 I5: SUB R4, R6, R9 ; uses R6 (from I4); ALSO writes R4 (WAR vs I2 which reads R4) I6: ADD R1, R2, R2 ; writes R1 again (WAW vs I1's write target; WAR vs I2's read) ```
Step 1 — classify dependences:
| Pair | Relationship | Type |
|---|---|---|
| I1 → I2 | I2 reads R1, I1 writes R1 | RAW |
| I2 → I3 | I3 reads R3, I2 writes R3 | RAW |
| I4 → I5 | I5 reads R6, I4 writes R6 | RAW |
| I2 → I5 | I5 writes R4, I2 reads R4 | WAR |
| I2 → I6 | I6 writes R1, I2 reads R1 | WAR |
| I1 → I6 | I1 writes R1, I6 writes R1 | WAW |
Step 2 — critical path (RAW-only) computation, with latencies LOAD=2, ADD/SUB=1, MUL=3:
| Instr | RAW predecessor | Predecessor finish | Own latency | Finish |
|---|---|---|---|---|
| I1 | — | 0 | 2 | 2 |
| I2 | I1 | 2 | 1 | 3 |
| I3 | I2 | 3 | 3 | 6 |
| I4 | — | 0 | 1 | 1 |
| I5 | I4 | 1 | 1 | 2 |
| I6 | — | 0 | 1 | 1 |
Critical path length cycles — the chain I1→I2→I3 dominates.
Step 3 — what a renaming machine achieves. The WAR edges (I2→I5, I2→I6) and the WAW edge (I1→I6) are all satisfiable by giving I5's and I6's destination registers fresh physical names; they add zero cycles to the true bound. So an ideal machine with enough functional units and a 2-wide issue could, in principle, finish everything in 6 cycles (bounded by the RAW chain), overlapping I4/I5/I6 entirely underneath the I1→I2→I3 chain, e.g.:
| Cycle | Executing |
|---|---|
| 1–2 | I1 (LOAD) ; I4 (ADD) |
| 3 | I2 (ADD, uses I1's result) ; I5 (SUB, uses I4's result) |
| 4–6 | I3 (MUL, uses I2's result) ; I6 (ADD, independent) issues cycle 1-2 window idle-fill |
Contrast this with a naive in-order machine without renaming, forced to respect WAR by delaying I5's write until after I2's read completes and I6's write until after I1's completion and I2's read: it could add several stall cycles beyond 6, purely from name conflicts that carry no real data — exactly the waste that register renaming exists to eliminate, foreshadowing Out-of-Order Execution.
Common pitfalls & traps
| Pitfall | Why it happens | Fix |
|---|---|---|
| Confusing "dependence" with "hazard" | Both sound like "these instructions can't be reordered" | Dependence is in the program; hazard is what the hardware does about it in a given schedule |
| Treating WAR/WAW as latency constraints | They look like RAW edges in a naive dependence graph | Only RAW edges bound the critical path; WAR/WAW are resolved by renaming with ~0 added latency |
| Assuming more functional units always helps | Width helps only independent work | Throughput ≤ n/width, but total time ≥ critical path length — whichever bound is larger dominates |
| Ignoring memory aliasing conservatism | Register dependences are easy to see; addresses are computed at runtime | Treat unresolved address pairs as RAW/WAR/WAW unless disambiguation proves otherwise |
| Forgetting control dependences entirely | Data hazards get all the attention in intro treatments | Branches gate which instructions are even fetched — model this separately from data-flow ILP |
| Off-by-one in forwarding-path stall counts | Miscounting whether a value is available at the start vs end of a stage | Trace stage-by-stage on a diagram before trusting a stall-cycle formula |
Pioneers & history
The formal vocabulary of data, anti-, and output dependences, along with the recognition that name dependences are removable via renaming while true dependences are not, crystallized in the compiler and computer-architecture literature of the 1960s–1980s, as machines began attempting instruction-level overlap beyond simple pipelining.
Origins · 1 minRobert M. Tomasulo and how this was discovered
Tomasulo designed the dynamic scheduling algorithm — reservation stations and register renaming via tags — that let hardware execute instructions out of program order while respecting true data dependences and eliminating name-dependence stalls. His algorithm is the direct ancestor of virtually every modern out-of-order core's scheduling core.
Where this matters
Every commercial superscalar processor — from ARM's Cortex-X and Apple's Firestorm/Icestorm cores to Intel's and AMD's desktop and server chips — is built around dynamically detecting and resolving exactly the dependences described here. Register renaming hardware (a Tomasulo-style rename table or reorder buffer) exists specifically to convert WAR/WAW name dependences into non-issues, freeing the machine to chase the RAW-bound critical path as aggressively as reorder-buffer size and functional-unit count allow. Compilers performing instruction scheduling, software pipelining, and loop unrolling for VLIW and DSP architectures rely on the same dependence graph to decide legal reorderings statically, since those architectures have no hardware renaming. Vectorizing and auto-parallelizing compilers use memory dependence analysis (alias analysis) to decide whether a loop's iterations are safe to run concurrently — directly building on the RAW/WAR/WAW taxonomy applied to array accesses instead of registers. Understanding these dependence limits is also what justifies why deeper pipelines and wider issue eventually hit diminishing returns without techniques covered next in Superscalar Execution and Out-of-Order Execution.
Ask the tutor: How does register renaming in a Tomasulo-style scheduler concretely map architectural registers to physical ones to eliminate WAR/WAW hazards during execution?
Is a hazard the same thing as a dependence?
No. A dependence is a property of the program: two instructions reference the same register or memory location in a way that constrains their relative order (RAW, WAR, or WAW), independent of any hardware. A hazard is what happens when a specific machine's execution timing threatens to violate that dependence — for example, a pipelined datapath that would read a register before an earlier instruction's write reaches it. The same dependence can produce a hazard on one implementation (a simple in-order pipeline without forwarding) and no hazard at all on another (a machine with full forwarding or renaming). Keeping this distinction sharp avoids the common error of assuming dependences dictate a fixed performance cost — the cost is determined by how the hardware resolves them.
Why do WAR and WAW hazards not count toward the true critical path length?
Because they are name dependences, not data-flow dependences: nothing in the actual computed value forces one instruction to wait for another. A WAR or WAW conflict only exists because two instructions happen to reuse the same architectural register name. A machine with register renaming assigns each write a distinct physical register, so the "conflict" evaporates — the reader of the old value and the writer of the new value use physically separate storage, with a rename table tracking which physical register each architectural name currently refers to. RAW dependences have no such escape hatch, since the consumer genuinely needs a value that does not exist until the producer computes it.
Can increasing issue width alone fix a program bounded by a long dependence chain?
No. If a program's longest RAW chain takes cycles (summing per-instruction latencies along that chain), no machine — no matter how many instructions it can issue per cycle — can finish in fewer than cycles, because each link in that chain genuinely must wait for the previous one's result. Wider issue only helps throughput on independent instructions that sit off the critical path; it does nothing to shorten the chain itself. This is why architects also pursue techniques like speculative execution, value prediction, and reducing per-operation latency (e.g., faster multipliers) to attack the critical path directly, rather than relying solely on wider superscalar issue.
How do compilers use dependence analysis differently from hardware?
Hardware (in an out-of-order core) discovers dependences dynamically, at runtime, using structures like reservation stations, reorder buffers, and rename tables, so it can react to actual addresses and values as they appear. Compilers, in contrast, must reason about dependences statically, before the program runs, often without knowing exact runtime addresses — this is why compiler dependence analysis for arrays and pointers is inherently conservative, sometimes blocking legal reorderings it cannot prove safe. Static scheduling matters most for architectures without hardware reordering, such as VLIW and many DSPs, where the compiler alone decides instruction order and must respect every true dependence it detects or conservatively assumes.
What role do control dependences play, since they're not data hazards?
A control dependence ties whether an instruction executes at all to the outcome of an earlier branch — it's a dependence on control flow, not on a data value. This is a fundamentally different constraint from RAW/WAR/WAW: it doesn't say "wait for a value," it says "don't even fetch/execute this yet, because we don't know if it belongs in the program's actual path." Naively, a pipeline would stall fetching until every branch resolves, which is why branch prediction and speculative execution exist: they let the machine guess the branch outcome and proceed, rolling back if wrong, converting what would be a stall into effectively free work most of the time.
Does eliminating all name dependences via infinite renaming mean a processor achieves the critical-path bound in practice?
Not quite. Infinite renaming removes WAR/WAW stalls, but real processors still face finite reorder-buffer and physical-register-file sizes, finite issue width, cache-miss latencies that lengthen individual instruction latencies unpredictably, and branch mispredictions that flush speculative work. The critical-path bound from the true RAW dependence graph is a theoretical lower bound assuming otherwise-ideal hardware; real out-of-order machines approach but don't perfectly hit it, because they must also pay for the discovery and bookkeeping of dependences (renaming logic, wakeup/select logic in the scheduler) within a fixed clock cycle and finite hardware budget.
Is memory dependence analysis ever exact, or always conservative?
It can be exact when addresses are statically known — for instance, fixed-offset stack accesses that a compiler can fully resolve. In general, though, addresses depend on runtime values (pointers, array indices, loop-carried computations), and proving two such addresses can never alias is undecidable in the general case. Compilers use techniques like alias analysis, points-to analysis, and runtime disambiguation checks to narrow this down, but must fall back to a conservative "assume dependence" whenever they can't prove independence, to guarantee correctness. Hardware faces the same issue and uses memory disambiguation predictors and load/store queues that check addresses dynamically, speculating that loads and stores are independent and rolling back if a real conflict is later detected.