FForge
← Labspasswork-01Pass work●●○○

Which pass did that? Bisect the pipeline

passwork/01_bisect_a_pass.cneeds: clang, opt (LLVM 17+)5 steps
0/5 steps
what this lab teaches

"It works at -O0 and breaks at -O2" is the most common compiler bug report there is, and the useless response is to read all 201 passes. The useful response is a binary search. -opt-bisect-limit=N runs the first N passes and skips the rest, so you can bisect on any observable property of the output and land on the exact pass in about eight runs. Here the property is benign — does the IR contain vector types — but the method is identical when the property is "does the program print the wrong answer". That is the whole point: practise the loop on something safe so you can run it under pressure on something that is not.

evidence to leave with
The pass number and name you landed on, and the one command that confirmed it by disabling that pass
first, get the code
run this
$ git clone https://github.com/adit4443ya/debug_labs.git && cd debug_labs
then build it
run this
$ cd passwork && ./build.sh
produces passwork/01_bisect.ll
walk it, one step at a time
run this
$ opt -O2 -opt-bisect-limit=-1 01_bisect.ll -o /dev/null 2>&1 | wc -l
what it printed on the machine that wrote this lab
201
-1 means "run everything and narrate it". Two hundred and one passes for
one twelve-line function. Reading that list top to bottom is not a plan.
run this
$ opt -O2 01_bisect.ll -S -o - | grep -c '<4 x i32>'
what it printed on the machine that wrote this lab
16

Sixteen vector-typed values in the final IR. At limit 0 there will be none. Somewhere between is the pass that introduced them. A good property is one word of output and no judgement: a count, or an exit status.

run this
$ for n in 200 100 150 125 137 142 144 143; do \
what it printed on the machine that wrote this lab
    printf "limit=%-4s vectors=%s\n" "$n" \
      "$(opt -O2 -opt-bisect-limit=$n 01_bisect.ll -S -o - 2>/dev/null | grep -c '<4 x i32>')"; \
  done
limit=200  vectors=16
limit=100  vectors=0
limit=150  vectors=11
limit=125  vectors=0
limit=137  vectors=0
limit=142  vectors=0
limit=144  vectors=11
limit=143  vectors=0

Eight runs to go from 201 candidates to one. The change happens when the limit reaches 144 and not at 143, so pass 144 is the one.

run this
$ opt -O2 -opt-bisect-limit=-1 01_bisect.ll -o /dev/null 2>&1 | sed -n '143,145p'
what it printed on the machine that wrote this lab
BISECT: running pass (143) InjectTLIMappings on sum_squares
BISECT: running pass (144) LoopVectorizePass on sum_squares
BISECT: running pass (145) InferAlignmentPass on sum_squares
LoopVectorizePass. Now you have a pass name, and every later question —
why did it fire, was it legal, what does it cost — has somewhere to start.
run this
$ opt -O2 01_bisect.ll -S -o - | grep -c '<4 x i32>'
what it printed on the machine that wrote this lab
16
run this
$ opt -O2 -disable-loop-vectorization 01_bisect.ll -S -o - | grep -c '<4 x i32>'
what it printed on the machine that wrote this lab
0

A bisect gives you a suspect; disabling the pass gives you a confession. Always do the second step — an off-by-one in the bisect is easy and the confirmation costs one command.

why the count changes between 144 and the end

Eleven at the boundary, sixteen at the end. Later passes widen and clean up what the vectorizer produced. That is normal, and it is why you bisect on "did the property appear" rather than on an exact number.

when the property is a wrong answer

Same loop, different test. Write a script that exits 0 when the program is correct and 1 when it is not, then bisect on the exit status:

run this
$ opt -O2 -opt-bisect-limit=$n bug.ll -o t.bc && clang t.bc -o t && ./t
what it printed on the machine that wrote this lab
That script is also exactly what llvm-reduce wants (lab 03), so writing it
once serves both.
take it further

1. Bisect for a different property: when does `sum_squares` stop being a call and get inlined into main? Grep the IR for `call.*sum_squares`. 2. Run with `-print-after=loop-vectorize` and read the IR it produced. 3. Compare the pass count at -O1, -O2 and -O3. The difference between -O2 and -O3 is smaller than most people assume — check before you believe anyone who says "just use -O3". ============================================================

read the source · passwork/01_bisect_a_pass.c
#include <stdio.h> #include <stdlib.h> int __attribute__((noinline)) sum_squares(const int *a, int n) { int total = 0; for (int i = 0; i < n; i++) total += a[i] * a[i]; return total; } int main(int argc, char **argv) { int n = argc > 1 ? atoi(argv[1]) : 8; int *a = malloc(n * sizeof *a); for (int i = 0; i < n; i++) a[i] = i; printf("%d\n", sum_squares(a, n)); free(a); return 0; }
◇
Finish the lab: write the evidence note

A lab is done when you can say what it showed on your machine — not when you have read it.