Out-of-Orderdependenciescritical pathdata flowlatencythroughput

Dependency Graphs: The Real Shape of Your Code

Program order is a line. What the machine actually obeys is a graph — and the longest path through that graph, not the number of nodes in it, is what sets the floor on how fast a loop can run.

Follow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
Two loops execute the same number of instructions and one is three times slower — what is the machine actually constrained by?
What you wrote
A sequence of statements, each one costing roughly what its operation costs. Total time looks like the sum of the parts.
What the hardware does
A directed graph where each operation is a node and each true data dependency is an edge. Independent nodes execute concurrently on separate ports; the achievable time is bounded below by the longest path through the graph, measured in operation *latencies*.
It replaces the additive mental model with the correct one. Once you see the graph, the two most common loop optimisations — breaking chains and increasing memory-level parallelism — stop being tricks and become the obvious response to a long critical path.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

Program order versus data flow

Consider four operations where B and C both read A's result and D combines theirs. Written down, that is four sequential lines. As a graph it is a diamond: after A, the machine can run B and C at the same time, and only D has to wait for both.

The time to execute the diamond is not the sum of four latencies. It is latency(A) + max(latency(B), latency(C)) + latency(D) — the longest path. Add a fifth independent operation hanging off A and, if a port is free, it costs nothing at all. This is why "I added a line, so it must be slower" is unreliable reasoning about a hot loop.

The graph also explains the shape of the fix. To speed up a chain-limited loop you either shorten the critical path (cheaper operations, fewer indirections, data already in cache) or you create additional independent paths for the scheduler to interleave. Nothing else helps, and in particular a faster clock helps only proportionally while a shorter chain can help far more.

needs xneeds xA: load xE: unrelated workB: x * 2C: x + 7D: B + C
UserLLMAgentToolDataDecisionHumanGuardrail

Latency and throughput are different numbers for the same instruction

Every execution unit has two characteristics that novices routinely conflate. Latency is how many cycles pass before the result is usable by a dependent operation. Throughput is how often a new, independent operation of that type can be started. A pipelined multiplier might accept a new multiply every cycle while each individual multiply takes several cycles to produce its answer.

That gap is the entire opportunity. A chain of n dependent multiplies costs roughly n × latency. A batch of n independent multiplies costs roughly n / throughput — potentially several times less for the same arithmetic. Which number you pay is determined solely by whether the operations depend on each other.

This is also the reason two microbenchmarks of "how fast is this instruction" can disagree by a large factor and both be correct: one measured a dependent chain and reported latency, the other measured independent work and reported throughput. When reading any instruction-cost table, the first question is always which of the two it lists — a trap Every Way a CPU Microbenchmark Lies covers in general.

Relative cost of the same arithmetic, arranged two ways. Unitless — the ratio is the lesson, and the exact figures differ by core. — 1 unit ≈ one independent operation issued at full throughputMICROARCH-SPECIFIC
N independent operations×1
N operations, chains of 2×2
N operations, chains of 4×3
N fully dependent operations×4
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
N independent operationsThroughput-limited: the machine starts one per cycle per port.
N operations, chains of 2Half the parallelism; the chain latency starts to show.
N operations, chains of 4Diminishing overlap as the graph narrows.
N fully dependent operationsLatency-limited: one result per operation latency, ports mostly idle.

Finding the critical path in real code

The practical procedure is short. Take one iteration of the hot loop. Draw an edge from every value produced to every operation that consumes it. Then find the longest path that also carries across iterations — the *loop-carried* dependency — because that one repeats n times and dominates everything else.

In the pointer-walk below, the loop-carried dependency is p = p->next. Every iteration must finish loading the next pointer before the following iteration can even begin computing its address, so the loop runs at one memory latency per element no matter how many execution ports sit idle. This is the mechanism behind Pointer Chasing: The Address You Do Not Have Yet and the reason a linked list loses to an array at identical asymptotic complexity (Both Are O(n). One Is Far Slower.).

In the indexed loop, the loop-carried dependency is only i = i + 1 — a cheap register increment. All the loads are independent of one another, so several can be outstanding simultaneously and the hardware prefetcher can run ahead. Same O(n), same element count, completely different graph.

Loop-carried dependency through memory
1p = head
2while p != null:
3 sum = sum + p->value
4 p = p->next # <-- next address is not known
5 # until this load completes
6
7# critical path per element: one full memory access
8# outstanding loads possible: one
Loop-carried dependency through a register
1for i in 0..n:
2 sum = sum + a[i] # address is i * size + base,
3 # computable immediately
4
5# critical path per element: one add
6# outstanding loads possible: many

Both are O(n) with one add per element. In the first, the address of the next access is itself the result of a memory load, so accesses strictly serialise and the machine can never have more than one outstanding. In the second, every address is known in advance from the index, so many loads proceed concurrently and the prefetcher can work. The difference is entirely in the shape of the dependency graph.

Key points

  • The machine obeys the dependency graph, not program order; the longest path through it bounds how fast the code can run.
  • Latency and throughput are different properties of the same instruction, and which one you pay depends on whether your operations are independent.
  • The loop-carried dependency is the one that matters, because it repeats once per iteration.
  • A dependency that runs through memory is dramatically worse than one that runs through a register, because its latency is orders of magnitude larger.
  • Adding independent work to a chain-limited loop is often free; adding it to a throughput-limited loop is not.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Decode → rename: false dependencies from register reuse are removed, leaving only true producer-to-consumer edges.
  2. 2
    Rename → scheduler: each operation records which physical registers it is still waiting on.
  3. 3
    Producer completes → wakeup: dependents whose last operand just arrived become eligible to issue.
  4. 4
    Scheduler → ports: eligible operations are selected each cycle, so independent branches of the graph proceed concurrently.
  5. 5
    Loop-carried edge → next iteration: if that edge passes through memory, the next iteration cannot start its address computation until a full memory access completes.
What people conclude from this — wrongly
  • "This loop does fewer operations, so it is faster." Fewer operations on a longer chain is usually slower.
  • "The instruction table says this costs 4 cycles." That is latency or throughput, and the two can differ by a large factor.
  • "Unrolling helps by removing the loop counter." Usually it helps by creating independent chains; the counter was cheap.
  • "O(n) is O(n), so traversal cost is comparable." Complexity counts operations, not the latency of the edges between them.
  • "The CPU is idle, so I am not compute-bound." Idle ports with a full window means you are dependency-bound, which is neither classic compute-bound nor memory-bandwidth-bound.

Consequences, controls and cost

What it causes
  • • Two loops with identical instruction counts can differ several-fold in runtime purely from graph shape.
  • • Reduction loops (sum, max, dot product) are chain-limited by default and respond well to multiple accumulators.
  • • Data structures that store the next address inside the current node cap memory parallelism at one outstanding access.
  • • Instruction-cost tables are ambiguous unless they say whether they list latency or throughput.
  • • Speedups from unrolling often come from breaking the dependency chain, not from removing loop overhead.
What you can do
  • • Identify the loop-carried dependency first — it is the only edge that repeats n times.
  • • Break reduction chains into several independent accumulators, then combine at the end.
  • • Replace memory-carried dependencies with index arithmetic where the data structure allows it.
  • • Hoist invariant computations out of the chain so the repeated path is as short as possible.
  • • Prefer data layouts that let addresses be computed rather than loaded ([[data-oriented-design]]).
How to see it
  • • Time a dependent chain against independent work for the same operation; the ratio exposes latency versus throughput on your machine.
  • • Add independent accumulators to a reduction and re-measure — a large improvement with identical instruction counts confirms a chain limit.
  • • Read IPC ([[ipc]]): a low IPC with low cache-miss counts and idle ports points at a dependency chain rather than memory.
  • • Count outstanding memory requests if your counters expose it; a value pinned near one during a traversal is the pointer-chasing signature.
  • • Compare against a version with the same accesses issued from precomputed indices to isolate the memory-carried edge.
What it costs
  • • Multiple accumulators change floating-point results by regrouping the sum; that may or may not be acceptable.
  • • Restructuring for independence usually costs readability and can obscure the algorithm being implemented.
  • • Layouts that make addresses computable (arrays, indices instead of pointers) trade insertion and deletion cost for traversal speed.
  • • Aggressive unrolling grows code and can turn a front-end-friendly loop into an instruction-cache problem.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • MICROARCH-SPECIFICOperation latencies, throughputs and the number of ports of each type vary by core. The ratio between a dependent chain and independent work is large on every out-of-order core, but its exact size is not portable.
  • GENERALThe graph model itself — longest path bounds runtime, loop-carried edges dominate — holds for any machine that overlaps execution at all, including in-order superscalar and VLIW designs.

Misconceptions

Claim
“Instruction count is a reasonable proxy for how long a loop takes.”
Reality
It is a proxy only when the loop is throughput-limited. For chain-limited code the count is nearly irrelevant; the longest dependency path sets the time, and adding independent instructions can be free.
Claim
“The compiler will break my dependency chains for me.”
Reality
It will for integer code where reassociation is legal. For floating point it must preserve your grouping unless you explicitly allow reassociation, because the result would differ (Why 0.1 + 0.2 Is Not 0.2 + 0.1's Problem).
Claim
“A dependency is a dependency; they all cost about the same.”
Reality
An edge through a register costs a few cycles. An edge through memory costs a cache or DRAM access — potentially two orders of magnitude more. Where the edge lives matters more than that it exists.

Apply it