$ objdump -d --no-show-raw-insn ./bin/04_microbench_pitfalls | sed -n '/<sum_keep>:/,/^$/p' | sed -n '14,30p'
what it printed on the machine that wrote this lab
401070: movq (%rdi,%rax,4),%xmm2
401075: movq 0x8(%rdi,%rax,4),%xmm3
40107b: pxor %xmm4,%xmm4
40107f: pcmpgtd %xmm2,%xmm4
401083: punpckldq %xmm4,%xmm2
401087: paddq %xmm2,%xmm0
...
40109b: add $0x4,%rax
40109f: cmp %rax,%rcx
4010a2: jne 401070 <sum_keep+0x30>
A real loop over an opaque n, vectorized: two 64-bit loads, sign-extend
(pcmpgtd + punpckldq is how baseline SSE2 widens int -> long), two paddq,
four elements per iteration. 0.44 ns/element = 256 MB in 30 ms = 8.6 GB/s,
i.e. this loop is memory-bandwidth bound on this laptop, and no amount of
AVX would change the number (try -march=native: TRY THIS #4 in lab 02).
Now `volatile`:
$ objdump -d --no-show-raw-insn ./bin/04_microbench_pitfalls | sed -n '/<sum_volatile>:/,/^$/p' | sed -n '12,18p'
what it printed on the machine that wrote this lab
401100: movslq (%rdi,%rcx,4),%rdx
401104: add %rdx,-0x8(%rsp)
401109: movslq 0x4(%rdi,%rcx,4),%rdx
40110e: add %rdx,-0x8(%rsp)
Scalar, and every element does a read-modify-write of the stack slot
holding the volatile accumulator: a store-to-load dependency of ~5 cycles
per element. 3.5 ns/element, 8x slower than the shipped code. You did
not measure "summing an array"; you measured "volatile". This is the
most common wrong benchmark on the internet.
HOW TO READ THE `warmup` ROWS:
pass 1 touches 256 MB of fresh pages: every 4 KiB page costs a minor page
fault (the kernel allocates and zeroes a page on first write). pass 2 pays
only memory bandwidth. Here 3.2x; on another run it was 10x — the
first-touch cost depends on what the kernel has to do (zeroing, page
reclaim, huge pages: THP is `madvise` on this box, so no). That is the
"warm-up" every benchmark framework discards. If your real workload runs
ONCE on fresh memory, pass 1 IS the real cost: then measure pass 1 on
purpose, and consider MAP_POPULATE / pre-faulting (lab 05).
HOW TO READ THE `timer` ROWS:
clock_gettime(CLOCK_MONOTONIC) is a vDSO call: no kernel entry, but ~27 ns
here. rdtsc ~12 ns; rdtscp+lfence (serialized, lab 05) ~24 ns. None can
time a 1 ns operation: put N of them in a loop and divide, or accept the
resolution floor. Everything you time includes the timer's cost once;
subtract it only if it is a meaningful fraction. (Numbers are for this
CPU; measure yours with `timer` before trusting sub-100 ns results.)
THE -O0 TRAP (the opposite lie):
$ ../tools/pin.sh -q ./bin/04_microbench_pitfalls.O0 dce
what it printed on the machine that wrote this lab
dce 85.941 ms 1.281 ns/iter sum unused -> loop deleted
$ ../tools/pin.sh -q ./bin/04_microbench_pitfalls.O0 constfold
what it printed on the machine that wrote this lab
constfold 0.002 ms 1.564 ns/iter constant input -> answer computed at build time
closedform 70.666 ms 1.053 ns/iter opaque input, but SCEV replaced the loop by a formula
At -O0 the dead loop "costs" 86 ms and the polynomial loop 71 ms.
Neither program exists in production. -O0 numbers describe the compiler's
worst-case scaffolding, not your algorithm. Benchmark what you ship.