Instrument · kernel autopsy Ⅱ
073: the invariant waste is dead — one tax remains
/alu/072 predicted what killing the pointer rebuild and gathers would reclaim. This is the follow-up with the measurement in hand: queries transposed once per tile into word-planar stack rows, the doc loop rewritten as AVX-512 intrinsics — and the emitted assembly re-read for what to do next.
Hover a segment. Same x-scale as the 072 ledger — the dashed region is the budget 073 reclaimed. The red that remains is the last answer-format tax in the pipeline.
The math came out exactly as designed: doc words broadcast from the stream, query rows as folded loads at compile-time offsets — the stack transpose means 0x00/0x40/0x80/0xc0 are the addressing, no pointers to distill, nothing to gather. LLVM even arranged the adds as a pairwise tree.
But one red block survived, and it's new: the store-out. The kernel finishes with 8 complete distances in one zmm — and then disassembles that register lane by lane: 8 extract/store pairs, each with its own tail-guard branch, ~24 uops un-vectorizing what the math just vectorized. The same answer-format law from 072, one stage later: the cost was never the arithmetic — it's the format the next consumer demands. 074 shipped the two-instruction fix: vpmovqd narrows the lanes, one wide store writes the group — and the measurement confirmed the prediction: +13% (1307 → 1471 QPS at C=2000), with C=500 reaching 1685 QPS. Toggle the ledger to see the measured shape.
C=2000, recall 0.9737 — bit-identical at every step. 2.26× from kernel work alone; recall never moved. Each step was one change, one measurement, one objdump.
Two things this arc teaches. First, the detour that didn't ship: written as safe scalar code, the planar loop scalarized — LLVM's popcount-idiom matcher doesn't recognize the shape it happily built itself in 072 from a gather pattern (822 and 736 QPS on the way to 1185). The kernel is intrinsics now; 012's “stay out of the autovectorizer's way” ends with an asterisk — when the pattern-matcher can't see your shape, you write the instructions yourself.
Second, the tiling knee moved 8 → 32 exactly as √(s/k) predicts when per-query tile state collapses from pointer machinery to 32 shared bytes in zmm rows. The scan now pulls ~44 GB/s of code stream at C=500 — within sight of the per-core streaming ceiling (068). The next factor of two lives in bytes or CCX-resident shards, not uops.
vpxorq 0xc0(%r8,%rdx,1),%zmm0,%zmm0qw[3][0..8] ^ doc.w3 — the offset IS the transpose; no gathervpxorq 0x80(%r8,%rdx,1),%zmm1,%zmm1qw[2] ^ doc.w2 (0x00/0x40/0x80/0xc0 = compile-time rows)vpopcntq %zmm0,%zmm08 lane-counts for w3vpopcntq %zmm1,%zmm18 lane-counts for w2vpaddq %zmm0,%zmm1,%zmm0w3+w2 — pairwise tree, shorter dep chain than serialvpxorq 0x40(...)/vpxorq (...) …w1, w0 same shape; final vpaddq → zmm0 = 8 complete distancesQPS measured on c8a.4xlarge (Zen5, 16 vCPU), 10M Snowflake arctic-256, exact ground truth, 3 reps (CV ≤ 3.5%). Uop counts read from the disassembly; the 074 column started as a prediction — the experiment existed to falsify it, and the measurement confirmed it instead at 1471 QPS (C=2000) and 1685 QPS (C=500).