Compiling the same source with the same binary does not always produce the same bytecode. The instruction sequence is stable; one register assignment moves.
Reproduce
go build -o /tmp/pas ./cmd/paserati/
cd tests
for i in $(seq 1 40); do /tmp/pas -bytecode scripts/bench_setindex.ts | shasum -a 256; done | sort | uniq -c
On a182fa2 that gives two distinct outputs, 6 of 40 runs taking the minority form. One binary, one input, no flags varied.
The entire difference
0314 OpIncPost R27, R24
-0324 OpUnsignedShiftRight R27, R21, R1
-0328 OpReturn R27
+0324 OpUnsignedShiftRight R26, R21, R1
+0328 OpReturn R26
(the OpIncPost line differs in the same register). Three lines, one register, everything else identical.
Of the five benchmark fixtures under tests/scripts/, only bench_setindex.ts shows it in 15 repeats each — bench_add.ts, bench_arith.ts, factorial.ts and matrix_mult.ts are stable. So it needs some particular shape of register pressure to surface.
Why I care about it beyond tidiness
I found this while building null controls for perf measurement, and it has a consequence there that is easy to miss: compilation happens per launch, so two runs of the same commit can execute different bytecode. That means byte-identical binaries do not imply identical executed code, and a control built on comparing build artifacts cannot see it.
BenchmarkSetIndex is the tightest benchmark in our suite (0.30% MAD/median), so whatever this costs is small — but it is an uncontrolled variable sitting underneath every measurement of that benchmark, and nothing was accounting for it.
Beyond benchmarking it also means -bytecode output is not reproducible, which makes it awkward as a debugging or golden-test artifact.
Guess at the cause
Most likely a map iteration order feeding register allocation — a for … range over a Go map somewhere in the allocator or in whatever tracks free/live registers. Go randomises that order per run, which fits both the per-run variation and the fact that only one function in one fixture is affected.
Happy to dig if useful, but you will find it much faster than I will.
Compiling the same source with the same binary does not always produce the same bytecode. The instruction sequence is stable; one register assignment moves.
Reproduce
On
a182fa2that gives two distinct outputs, 6 of 40 runs taking the minority form. One binary, one input, no flags varied.The entire difference
(the
OpIncPostline differs in the same register). Three lines, one register, everything else identical.Of the five benchmark fixtures under
tests/scripts/, onlybench_setindex.tsshows it in 15 repeats each —bench_add.ts,bench_arith.ts,factorial.tsandmatrix_mult.tsare stable. So it needs some particular shape of register pressure to surface.Why I care about it beyond tidiness
I found this while building null controls for perf measurement, and it has a consequence there that is easy to miss: compilation happens per launch, so two runs of the same commit can execute different bytecode. That means byte-identical binaries do not imply identical executed code, and a control built on comparing build artifacts cannot see it.
BenchmarkSetIndexis the tightest benchmark in our suite (0.30% MAD/median), so whatever this costs is small — but it is an uncontrolled variable sitting underneath every measurement of that benchmark, and nothing was accounting for it.Beyond benchmarking it also means
-bytecodeoutput is not reproducible, which makes it awkward as a debugging or golden-test artifact.Guess at the cause
Most likely a map iteration order feeding register allocation — a
for … rangeover a Go map somewhere in the allocator or in whatever tracks free/live registers. Go randomises that order per run, which fits both the per-run variation and the fact that only one function in one fixture is affected.Happy to dig if useful, but you will find it much faster than I will.