The last concept measured a ratio — 2.7× between dependent and independent execution — without saying what a dependency is. That is backwards, and this concept puts it the right way round: an instruction costs a latency because something constrains when it may start, and the cost is a consequence of the constraint rather than a property of the instruction.
There is a second thing a dependency can constrain, and this course found it by accident. Two byte-identical loops in one binary, differing only in their address, measured 0.79 and 2.13 ticks per iteration. No data dependency explains that, because the code is the same code. What differs is where the front end finds it.
$ objdump -d cycbench | grep -A9 -E '<b_dep1>:|<b_ind1>:' 0000000000002c40 <aligned_0>: 0000000000002cc0 <branchless>: 0000000000003840 <b_dep1>: $ ./noise2 64-byte aligned body min 1.0607 max 1.2190 spread 14.9% same body at offset 32 min 0.9956 max 1.2104 spread 21.6%
So this concept has two halves that look unrelated and are not: a data dependency is a constraint on when a register may be read, and an address is a constraint on when a block of bytes may be fetched. Both are the same kind of thing — a rule that says “not yet” — and both are invisible in the instruction stream.
What a dependency is, in the smallest useful terms:
Which gives the rule that decides performance, and it is a one-liner:
Now the second half, which is the same shape applied to bytes rather than registers. The front end has to fetch instructions, and it fetches them in blocks that begin at boundaries determined by the address. A loop that sits entirely inside one block is fetched efficiently; one that straddles a boundary needs two. The block size is not a matter of opinion — the instrument concept measured the cache line at 64 bytes from getconf and the same number out of /sys.
That asymmetry is the point worth carrying forward. A register dependency is a property of the code and the hardware works around it by renaming. An address dependency is a property of the link and the hardware cannot work around it at all. Which means the linker is a performance tool, whether or not it knows it — and the linker-script course has been placing sections without ever having had a timing in its model.
One loop body, byte for byte identical, emitted at eight offsets within a 64-byte boundary:
$ ./cycbench 2>&1 | sed -n '/7. ALIGNMENT/,$p'
7. ALIGNMENT: one body, seven addresses
-------------------------------------
The SAME 16-byte loop (add; dec; jnz; mov), byte for byte,
preceded by identical padding. Only the address changes.
body min ticks vs floor
loop at offset 0 mod 64 0.7645 1.15x
loop at offset 8 mod 64 0.8334 1.26x
loop at offset 16 mod 64 0.7716 1.16x
loop at offset 24 mod 64 0.8274 1.25x
loop at offset 32 mod 64 1.3533 2.04x
loop at offset 40 mod 64 0.6638 1.00x
loop at offset 48 mod 64 0.8394 1.26x
loop at offset 56 mod 64 (CROSSES) 0.8554 1.29x
fastest 0.6638 slowest 1.3533 ratio 2.04x
2.04×, for identical instructions. That is the finding, and it is larger than anything in the latency table — bigger than the dependent-versus-independent ratio, bigger than the cost of the operations themselves. Where the linker puts a loop can matter more than what the loop does.
And here is where the honesty has to be, because the obvious story is wrong. The loop body is 12 bytes, so offsets 0–52 fit inside one 64-byte line and offset 56 does not. If crossing a line boundary were the whole mechanism, offset 56 would be the slow row. It is not — offset 32 is, and offset 40 is the fastest of all.
So the effect is claimed and the cause is not. That is the whole discipline in one measurement, and it is worth being explicit about why it is the right call rather than a cowardly one: a mechanism you have guessed is a mechanism a reader will act on, and acting on a wrong one costs them an afternoon. An admitted unknown costs them nothing but the question.
And the practical consequence is immediate and does not need the mechanism. Every bench function in the artifact is marked aligned(64), and making that change was the single largest improvement to the instrument — it is what turned the latency table from a set of numbers that contradicted each other into monotonic series. The rest of this course’s numbers exist because of that one attribute.
The other dependency in the measurement was there from the start and hid everything for several iterations. Every loop carries one, in its own counter.
The fix is to unroll, and it is worth being precise about which unrolling, because there are two and they fix different things.
There is a third dependency hiding in the same measurement, and finding it is what made the flags section honest. The adc in the branchless variant reads the carry flag that the ror before it wrote. That is a RAW dependency on a register with no rename:
$ ./cycbench 2>&1 | sed -n '/4. THE FLAGS/,/5. BRANCHES/p' body min ticks vs floor loop floor 0.6630 1.00x 1 TEST (WRITE flags only) 0.7385 1.11x 2 TEST (WRITE flags only) 0.6833 1.03x 4 TEST (WRITE flags only) 0.9582 1.45x 4 ROR (a CHAIN on one register) 3.5259 4.23x 1 ROR+ADC (read AFTER write) 0.9427 1.13x 2 ROR+ADC (read AFTER write) 1.7822 2.14x 4 ROR+ADC (read AFTER write) 3.7130 4.46x
Read the third row and the fifth. Four TESTs, which all write the flags, cost 1.45× the floor — nearly free. Four RORs cost 4.23× — and the difference is that they all target one register, so they are an ordinary RAW chain of length 4.
And the first explanation this course wrote down was wrong. It said the flags register has no rename, so flag-writing instructions serialise. The measurement says otherwise. So the obvious replacement was tried — “reading the flags is what costs” — and four ROR+ADC pairs measured 4.46× against 4.23× for the RORs alone, a difference of 1.05×. Also wrong.
Three candidate explanations, two retracted, and the survivor is the boring one: it was always an ordinary dependency, and the flags were a red herring. That is recorded in the artifact’s own output and asserted by the crosscheck, so it cannot quietly come back.
$ cd courses/exe/assets/samples $ ./cycbench 2>&1 | sed -n '/4. THE FLAGS/,/5. BRANCHES/p' $ ./cycbench 2>&1 | sed -n '/7. ALIGNMENT/,$p' $ python3 crosscheck.py 2>&1 | sed -n '/^H /,/^I /p' $ python3 crosscheck.py 2>&1 | sed -n '/^I /,$p'
Then go looking for dependencies in code you did not write:
1. Classify every instruction in a real loop as
RAW, WAR or WAW, by hand. Take something
small:
objdump -d --section .text yourprog \
| sed -n '/<yourfunc>:/,/^$/p'
For each pair (A, B) ask: does B read a
register A wrote? That is RAW and it is the
only kind that survives renaming.
Now do the same for the ADDRESSES: which
blocks does this loop span, and does it cross
a 64-byte boundary? (You can count with the
ISA course's decoder -- it reports offsets,
and 0x40 bytes apart is a line.)
2. Break a WAR/WAW on purpose and show the core
does not care. Take a register that is written
twice in a row and give the two writes
different destinations:
mov rax, 1
mov rax, 2 # WAW on rax
ret
mov rbx, 1 # the second write goes
mov rax, 2 # elsewhere: no WAW
Time both. (Expect no difference at this
size -- the effect of WAW elimination is real
but you need enough of them to see it. Build
a loop with 64 WAW pairs and compare with 64
RAW pairs; the ratio is the renaming benefit
and it should be close to the width.)
3. Find a RAW chain that is not obvious, which
is the kind that actually costs time:
a = b + c; d = a * e; f = d - g;
Three operations, but a LONGEST chain of 3,
not 3 independent ones. Now:
a = b + c; d = x * e; f = y - g;
Same operation count, longest chain 1. This
is the whole optimisation and it is what a
compiler does when it reassociates floating
point -- which it may NOT do without
-ffast-math, because it changes the result.
That is a dependency bought with correctness,
and it is worth finding one in a compiler
you use.
4. Now measure the address effect properly, at
all 64 offsets, and see whether YOUR curve
is periodic:
for (off = 0; off < 64; off++)
build a binary with a loop at that offset
time it
(cycbench does 8 of the 64 with one binary.
Doing all 64 needs 64 builds, or one build
with 64 copies at 64 offsets. Either way,
the shape is the result. This machine's
curve is NOT periodic in 64 -- it is not
periodic at all -- and your front end may
well be different. The METHOD is what
transfers: identical bytes, varying address,
and treat the curve as a measurement of the
front end rather than an explanation of it.)
5. Finally, connect the two halves, because they
are one thing and this is the exercise that
shows it. Write a loop where the data
dependency is short AND the address is bad,
and one where each is the opposite, and
confirm the two effects are additive rather
than one masking the other.
// short chain, well aligned
for (i = 0; i < n; i++) { a[i] = b[i] + c[i]; }
// long chain, well aligned
for (i = 0; i < n; i++) { s = s * 3 + 1; }
// short chain, badly aligned
// (pad the loop to start at offset 32)
If the effects are additive you will see
three distinct times. If the bad alignment
MASKS the dependency cost, the two short-
chain versions will measure the same -- and
that would be the more interesting result,
because it would mean the front end, not the
data path, is the limit for a large class
of real code.
Exercise 5 is the one that unifies the concept, and its outcome is genuinely uncertain, which is why it is worth doing. If the address effect and the dependency effect are additive, this course has two independent stories. If they mask each other, the front end dominates and the latency table in module 1 describes a machine that most real code never reaches. Either answer is worth having, and the measurement is twenty lines long.
Exercise 2 is the one that makes renaming real rather than a slogan. The ratio between a WAW-heavy loop and a RAW-heavy loop of the same length is the renaming benefit, and it should come out close to the execution width from the latency concept — two completely separate measurements landing on the same number, which is the kind of agreement the collection treats as evidence.
The connection to the linker-script course is the practical payload of this concept, and it is a debt that course could not even see. That course found the default script’s section placement rules and their effect on the output, and had no reason to ask whether the placement cost anything — because nothing in the format records a time. This concept supplies the missing term: section alignment is not tidiness, it is up to a factor of 2.5 in execution time for a loop that happens to straddle a boundary, and it is fixed at link time in a way the hardware cannot undo. Every linker discussion in the collection up to this point has been about what goes where; this is the first measurement of what where costs.
The connection to the PIC-cost concept is a genuine trade the two courses disagree about, and the disagreement is the point. That course measured nine instructions against seventeen for the same computation and concluded PIC costs more. This concept supplies the reason that conclusion may be wrong: seventeen instructions with a longer chain is not necessarily slower than nine with a shorter one, because the extra instructions may be independent. Both are measurable, neither was measured against the other, and the resolution needs a timing experiment that spans both courses. The encoding-limits concept is the other half of that argument, since it is the disp32 that makes the extra instruction necessary.
Two connections outward. The SIB concept measured the one-byte instruction that exists so a compiler can write base + index*scale without a scratch register, and explicitly said it saves about twelve bytes and two instructions. This concept is the measurement that makes that saving worth having: two instructions that do not destroy a register are two more independent operations, and the marginal cost of an independent operation was measured at about a quarter of a dependent one. So the ISA course’s encoding feature and this course’s execution measurement are the same fact seen from the compiler’s side and the CPU’s, and each is useless without the other.
And the connection to the object course’s oracle loop is methodological rather than technical, and it is the reason this concept exists in the shape it does. That concept established the discipline of checking a value against a source that did not produce it. Here the discipline produced the course’s single most useful result by accident — the apparent noise turned out to be a fixed per-address penalty — and it did so because a variance figure is a claim about your setup before it is a claim about your machine, and nobody had looked. The check that would have caught it was the obvious one: are these two functions byte-identical? They were not supposed to differ, and they did not — which is exactly what made the difference visible.
One limit, and it bounds the concept on both sides. The address effect is measured and its mechanism is not, so this concept can say “the address of a loop is worth up to 2.5×” and not why; and the data-dependency half is measured on integer add in a synthetic loop, so the latency is this machine’s integer-add latency and nothing more general. No floating point, no vector unit, no multiply, and no memory operand was measured — the last of those is where the real limit usually lives, and it is the subject of the memory-hierarchy course.