Interviews ask you to look at IR before and after and say which transformation
ran and why it was legal. The way to get good at that is not to read the whole
-O2 output — 217 passes report a change on a twelve-line function — but to
run one pass at a time on input small enough that the diff is obvious, until
you recognise each transformation on sight.
The input is hand-written IR rather than clang output, so there is nothing between you and the change.
Nothing to build. 02_read_what_a_pass_did.ll is the input.
$ opt -passes=gvn -S 02_read_what_a_pass_did.ll -o - | sed -n '/@redundant_load/,/^}/p'
define i32 @redundant_load(ptr %p) {
entry:
%a = load i32, ptr %p, align 4
%sum = add i32 %a, %a
ret i32 %sum
}
Two loads became one and %b was replaced by %a everywhere. Say why it is
legal: nothing writes through %p between them, and %p is not volatile, so
both loads must observe the same value.
Now make it illegal, and watch alias analysis decide. Put a store through a different pointer between the loads:
define i32 @redundant_load(ptr %p, ptr %q, i32 %v) { %a = load i32, ptr %p store i32 %v, ptr %q %b = load i32, ptr %p %sum = add i32 %a, %b ret i32 %sum }
$ opt -passes=gvn -S that.ll -o -
%a = load i32, ptr %p, align 4
store i32 %v, ptr %q, align 4
%b = load i32, ptr %p, align 4 ; both loads kept
%sum = add i32 %a, %b
GVN backs off, because %q might alias %p. Add one word:
$ sed 's/ptr %p, ptr %q/ptr noalias %p, ptr noalias %q/' that.ll | opt -passes=gvn -S -
%a = load i32, ptr %p, align 4
store i32 %v, ptr %q, align 4
%sum = add i32 %a, %a ; redundant load gone again
That is the entire value of restrict in C, in three lines of IR: the
optimization was always legal, the compiler just could not prove it. Worth
running once yourself — it is the clearest demonstration of alias analysis you
will find, and it answers the interview question about restrict with a
transcript instead of an opinion.
One more, because it surprises people: store a constant rather than %v
through %p itself, and GVN does not keep two loads — it forwards the stored
value directly (add i32 %a, 7). That is store-to-load forwarding, a different
optimization wearing the same pass's name.
$ opt -passes=dce -S 02_read_what_a_pass_did.ll -o - | sed -n '/@dead_arithmetic/,/^}/p'
define i32 @dead_arithmetic(i32 %x) {
entry:
%r = add i32 %x, 1
ret i32 %r
}
The multiply is gone. Legal because the instruction has no users and no side
effects — the second half matters, which is why DCE will not touch a store or
a call it cannot prove is pure.
$ opt -passes=simplifycfg -S 02_read_what_a_pass_did.ll -o - | sed -n '/@constant_branch/,/^}/p'
define i32 @constant_branch(i32 %x) {
entry:
%always = icmp sge i32 4, 3
%x. = select i1 %always, i32 %x, i32 0
ret i32 %x.
}
It flattened the branch into a select — and left icmp sge i32 4, 3 sitting
there, obviously constant, unfolded. That is not a bug. SimplifyCFG's job is
control flow; folding constants is InstCombine's. Passes are deliberately narrow.
$ opt -passes=instcombine -S 02_read_what_a_pass_did.ll -o - | sed -n '/@constant_branch/,/^}/p'
define i32 @constant_branch(i32 %x) {
entry:
br i1 true, label %yes, label %no
yes:
ret i32 %x
no:
ret i32 0
}
InstCombine folds the comparison to true — and leaves the dead block, because
deleting blocks is SimplifyCFG's job. Each pass does half. Run both:
$ opt -passes='simplifycfg,instcombine' -S 02_read_what_a_pass_did.ll -o - | sed -n '/@constant_branch/,/^}/p'
define i32 @constant_branch(i32 %x) {
entry:
ret i32 %x
}
Both orders reach the same answer here. That is why the real pipeline runs
simplifycfg and instcombine repeatedly rather than once: each creates work
the other can then do, and the fixed point is reached by alternating.
$ opt -O2 -print-changed 02_read_what_a_pass_did.ll -o /dev/null 2>&1 | grep -c "IR Dump After"
269
-print-changed prints a dump only when a pass actually modified the IR, which
is the difference between a readable log and forty thousand lines.
-print-after=<pass> narrows it to one pass; -print-after-all is the blunt
version and is almost never what you want.
For any before/after, answer three things in order:
The third is what separates someone who has read about passes from someone who has debugged one.
volatile and re-run. GVN backs off even with noalias. Say
why volatile is different from "might alias" — one is about what the value
is, the other about whether the access itself is observable.opt -passes=gvn -aa-trace or -print-changed on the aliasing version
to see the query being answered rather than inferring it from the result.readnone to the
declaration and it disappears.A lab is done when you can say what it showed on your machine — not when you have read it.
It decides which problems come first, which drills open, which labs you see and how the Learn surface is organised. Change it any time from the switch in the top bar, or press Escape to look around first.