Repository navigation
Description
Split out of #7128 finding E, because it is not a representation-selection
problem and it blocks a general technique.
What happens
Compiling the same file twice with identical flags produces different object
bytes on aarch64 Linux and identical bytes on macOS.
Measured (#7128): all 26 benchmarks/repsel_census workloads were
nondeterministic on a Raspberry Pi 5 (aarch64, Linux), 0 of 26 on an M1 Mac
mini. On suite_01_startup exactly 10 bytes differ.
Cause
crates/perry-codegen/src/linker.rs:294-306 names the temporary IR file after
the process:
let pid = std::process::id();
let counter = TEMP_NONCE_COUNTER.fetch_add(1, Ordering::Relaxed);
let wall_nonce = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos())…;
let ll_path = tmp_dir.join(format!("perry_llvm_{pid}_{wall_nonce}_{counter}.ll"));clang records that path as the translation unit's file name, and on ELF it
lands in the emitted object. Mach-O keeps it in the debug map rather than the
.o, which is why macOS looks clean — the same non-determinism is present, it
just is not in the bytes being compared.
The uniqueness requirement is real (#509: two rayon workers racing the same
microsecond overwrote each other's .ll), so the fix is not "drop the nonce" —
it is to make the name a function of the content rather than of the clock,
e.g. perry_llvm_<hash(ll_text)>_<counter>.ll, or to pass clang
-fdebug-compilation-dir / -frandom-seed and normalise the recorded name.
Why it matters beyond reproducible builds
Object-hash A/B is the cheapest and least deniable instrument this project has
for "did this change what ships" — #7113, #7119, #7121, #7128 all rest on it,
and #7128's whole "which representation moved which workload" table is an
object-hash table. That instrument does not work on Linux today. Every
object-level claim in #7128 had to be taken on the Mac mini, which in turn
meant no instruction-retired counter (macOS has no unprivileged one), so
"emission changed" and "it got slower" could not be measured on the same host.
It also defeats:
- ELF
.textsize deltas as a signal (already unreported in repsel: the coverage work converted exactly as predicted, and almost none of it is faster (one −4.1% win, one +14.9% regression) #7128 for this
reason plus 4 KB quantisation); - the
--no-cacheobject A/B inbenchmarks/repsel_census/README.md; - the emission half of
census-knob-isolation(repsel: the coverage work converted exactly as predicted, and almost none of it is faster (one −4.1% win, one +14.9% regression) #7128 follow-up), which detects
the host and deliberately skips rather than reporting 26 phantom diffs.
Repro
perry compile benchmarks/suite/01_startup.ts -o /tmp/a.o --no-link --no-cache
perry compile benchmarks/suite/01_startup.ts -o /tmp/b.o --no-link --no-cache
# on aarch64 Linux: the two "Wrote object file:" paths differ in 10 bytes(Compare the paths the compiler prints, not the -o arguments — --no-link
does not honour -o.)
Acceptance
Compiling every census workload twice with identical flags produces
byte-identical objects on Linux as well as macOS, and
census-knob-isolation --require-emission passes on the Pi.
Activity
changelog: key #7131 fragment on PR #7135
fix(codegen): content-address LLVM temp IR names (#7131) (#7135)
fix(codegen): restore per-process uniqueness of the LLVM temp OBJECT …
changelog: #7131 Linux emission determinism (PR #7140)
changelog: fold in the measured Linux + macOS numbers (#7131)
docs(census): correct the Mach-O claim to what was measured (#7131)
changelog: matrix verdict, FAIL=0 over 546 rows (#7131)
fix(codegen): per-process temp object paths, and make emission determ…
exp: diff arms share one module identifier (ELF records it in STT_FILE)
exp: diff arms share one module identifier (ELF records it in STT_FILE)
In-process LLVM backend: native construction via the C API, opt-in (#…