Skip to content

docs(perf): -C target-cpu=native measures neutral, and the return leg is what proves it - #228

Merged
doublegate merged 3 commits into
mainfrom
docs/target-cpu-native-is-neutral
Jul 31, 2026
Merged

docs(perf): -C target-cpu=native measures neutral, and the return leg is what proves it#228
doublegate merged 3 commits into
mainfrom
docs/target-cpu-native-is-neutral

Conversation

@doublegate

Copy link
Copy Markdown
Owner

The structural-waste search (#227) was one axis. How the compiler emits the code is a separate one, and it was unexamined until now.

Nothing in the workspace sets target-cpu, so every build targets baseline x86-64 while this host offers AVX2 and BMI2. That looks like free headroom.

It is not

A-B-A, frame_cost_probe on Super Mario 64, --release --features fast-scheduler:

leg variant frame mean
A baseline (x86-64) 93.777 / 93.689 ms
B RUSTFLAGS="-C target-cpu=native" 93.399 / 93.056 ms
A baseline, again 92.068 ms

The third A leg came in below both B legs.

A two-leg comparison would have reported a ~0.5% win. The return leg shows it was drift — mean A 93.178 against mean B 93.227, 0.9995×, neutral. This is the third time in this project that A-B-A has overturned a two-leg result, after the #[inline] phantom regression and the release-guard phantom improvement.

Unsurprising in hindsight: the hot code is scalar integer interpretation and pointer chasing, not the vectorizable arithmetic AVX2 would help.

The portability cost never had to be argued — a target-cpu=native binary does not run on older hosts, which would be a real price for a measured nothing. Added to the ruled-out list as 5b so it is not retried.

PGO is the one codegen lever still untested

Named explicitly, with the warning not to assume it is neutral because this was. The mechanisms do not overlap: PGO drives branch layout and inlining from a real profile rather than instruction selection, and interpreters are the workload it classically helps most.

It needs two full LTO rebuilds plus an instrumented run, and it changes the release process rather than a config line — so it is recorded as an open avenue rather than attempted in passing.

Refs #55.

… is what proves it

The structural-waste search was one axis; how the compiler EMITS the code is a
separate one, and it was unexamined. Nothing in the workspace sets target-cpu, so
every build targets baseline x86-64 while this host offers AVX2 and BMI2 -- which
looks like free headroom.

It is not. A-B-A, frame_cost_probe on Super Mario 64, --release --features
fast-scheduler:

  A  baseline (x86-64)                93.777 / 93.689 ms
  B  RUSTFLAGS="-C target-cpu=native" 93.399 / 93.056 ms
  A  baseline, again                  92.068 ms

THE THIRD A LEG CAME IN BELOW BOTH B LEGS. A two-leg comparison would have
reported a ~0.5% win; the return leg shows it was drift. Mean A 93.178 against
mean B 93.227 -- 0.9995x, neutral. Unsurprising in hindsight: the hot code is
scalar integer interpretation and pointer chasing, not the vectorizable arithmetic
AVX2 would help. The portability cost never had to be argued, since a
target-cpu=native binary does not run on older hosts and that would be a real
price for a measured nothing.

Added to the ruled-out list as 5b so it is not retried.

PGO is named as the one codegen lever still untested, with the explicit warning
NOT to assume it is neutral because this was: it works on branch layout and
inlining driven by a real profile rather than on instruction selection, and
interpreters are the workload it classically helps most. It needs two full LTO
rebuilds plus an instrumented run and changes the release process rather than a
config line, so it is recorded as an open avenue rather than attempted in passing.

Refs #55.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@doublegate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 8b8e9e51-ba15-4302-987b-8e711e4996f0

📥 Commits

Reviewing files that changed from the base of the PR and between bf15924 and 7089fe1.

📒 Files selected for processing (1)
  • docs/accuracy-ledger.md
📝 Walkthrough

Summary by CodeRabbit

  • Documentation
    • Added performance guidance covering native CPU code generation benchmark results.
    • Documented baseline measurements and portability considerations.
    • Recorded that the native CPU option was not adopted after showing no measurable improvement.
    • Identified PGO as a potential alternative for future evaluation.

Walkthrough

Updates performance documentation with neutral target-cpu=native benchmark results, baseline comparisons, portability considerations, and PGO as an untested alternative.

Changes

Code-generation documentation

Layer / File(s) Summary
Benchmark analysis and build guidance
docs/performance.md
Records A-B-A measurements comparing baseline x86-64 builds with target-cpu=native, documents portability limits, and identifies PGO as untested.

Estimated code review effort: 1 (Trivial) | ~3 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 8 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is related to the documented performance investigation but violates the Conventional Commits limit (87 > 72 characters) and is not in imperative mood. Shorten the subject to 72 characters or fewer and rewrite it in imperative mood, without a trailing period.
Measured, Never Tuned ⚠️ Warning The PR adds 93.x ms timings and AVX2/BMI2 claims with no manual/wiki citation or accuracy-ledger entry, violating the measured-constant provenance rule in docs/accuracy-ledger.md §What this file is... Add the new timing/hardware claims to docs/accuracy-ledger.md with their measurement method and provenance, or remove them from the PR.
✅ Passed checks (8 passed)
Check name Status Explanation
Description check ✅ Passed The description is directly related to the documentation change and explains the benchmark result, portability consideration, and PGO follow-up.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Oracle Number Is Stated ✅ Passed PASS: only docs/performance.md changed. The rule explicitly passes docs-only changes, so no n64-systemtest failing-assertion count is required.
Docs-As-Spec Sync ✅ Passed Docs-as-spec sync is not triggered: the PR changes only docs/performance.md; no rustyn64 chip-crate code or observable behaviour changed.
Changelog Entry For User-Visible Changes ✅ Passed Only docs/performance.md changed; it records a neutral, unadopted benchmark and no shipped behaviour, so the CHANGELOG rule is not triggered.
Unsafe Stays Out Of The Chip Crates ✅ Passed PR changes only docs/performance.md; no unsafe syntax was added, and all chip crates plus rustyn64-core retain #![forbid(unsafe_code)].

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/performance.md`:
- Around line 629-631: Revise the conclusion around the “-C target-cpu=native”
result and the corresponding explanation near the codegen-levers discussion to
scope the finding only to this benchmark workload. State that A-B-A showed no
measurable gain here, and label claims about instruction-selection binding or
pointer chasing as hypotheses unless supported by differential profiling
evidence; do not present aggregate results as proof of mechanism.
- Around line 858-860: Update the portability statement in the target-cpu=native
discussion to say that binaries may not run on older hosts, rather than
asserting they always do not run. Preserve the explanation about host-specific
instructions and potential illegal-instruction failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: e29c0406-d57b-4f59-8c9e-f62364fb39de

📥 Commits

Reviewing files that changed from the base of the PR and between 7a90b75 and 8fc9d54.

📒 Files selected for processing (1)
  • docs/performance.md

Comment thread docs/performance.md Outdated
Comment thread docs/performance.md Outdated
…or it was not

Review round one on #228. Both adopted, and the first is a repeat of a mistake
this session had already named.

I explained the null result with a mechanism -- "the hot code is scalar integer
interpretation and pointer chasing, not the vectorizable arithmetic AVX2 would
help". The A-B-A establishes no measurable gain ON THIS BENCHMARK. It does not
establish that the emulator is generally not instruction-selection-bound, and an
aggregate frame time conflates every cost in the frame, so it cannot isolate a
cause at all. That is the same shape as the earlier "the profile over-predicted"
claim in the VI work: an untested story attached to a measurement, which is how a
plausible mechanism turns into a repeated citation.

The explanation is kept -- it is consistent with the per-line survey, where no
vectorizable inner loop appears anywhere -- but it is now labelled an INFERENCE,
with what would actually settle it (differential profiling of the two builds
rather than one number from each). The conclusion is scoped to the benchmark in
both the ruled-out entry and the section.

Second: "does not run on older hosts" overstated it. target-cpu=native MAY emit
instructions an older host does not implement, and such a binary MAY fail there
with an illegal instruction -- it depends on which features LLVM selects and what
the host supports. Reworded, and the portability point stands on being a real risk
rather than a certainty.

Gates: fmt, clippy -D warnings, test --workspace, rustdoc -D warnings,
check_en_us.sh, markdownlint -- ALL-GATES-OK.
@doublegate

Copy link
Copy Markdown
Owner Author

Round one. Both adopted — and the first is a repeat of a mistake this session had already named, which is worth saying plainly.

1. "Limit the conclusion to this workload; mark the explanation as inference" — ADOPTED, and correct.

I wrote that the null result was "unsurprising in hindsight: the hot code is scalar integer interpretation and pointer chasing, not the vectorizable arithmetic AVX2 would help."

The A-B-A establishes no measurable gain on this benchmark. It does not establish that the emulator is generally not instruction-selection-bound, and an aggregate frame time conflates every cost in the frame, so it cannot isolate a cause at all.

That is the same shape as the "the profile over-predicted" claim I had to retract in the VI work two PRs ago: an untested story attached to a real measurement, which is precisely how a plausible mechanism becomes a repeated citation. Catching it twice in one session says the reflex is the problem, not the individual sentence.

The explanation is kept — it is consistent with the per-line survey, where no vectorizable inner loop appears anywhere — but it is now labelled an inference, with what would actually settle it stated: differential profiling of the two builds, not one number from each. The conclusion is scoped to the benchmark in both the ruled-out entry and the section body.

2. "Qualify the portability claim" — ADOPTED.

"Does not run on older hosts" overstated it. target-cpu=native may emit instructions an older host does not implement, and such a binary may fail there with an illegal instruction — it depends on which features LLVM actually selects and what the older host supports. Reworded, and the portability argument stands on being a real risk rather than a certainty, which is enough given the performance side measured nothing.

Gates: fmt, clippy -D warnings, test --workspace, rustdoc -D warnings, check_en_us.sh, markdownlint. ALL-GATES-OK.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/performance.md`:
- Around line 629-631: Update the accuracy-ledger entry corresponding to the
target-cpu=native finding described in the performance documentation. Record the
benchmark, A-B-A comparison method, measured result and drift interpretation,
and explicitly capture the portability and PGO limitations while noting the
ruled-out alternatives required by the ledger’s evidence rules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f9e0460c-0dfb-46b5-beae-4dcb843f11dc

📥 Commits

Reviewing files that changed from the base of the PR and between 8fc9d54 and bf15924.

📒 Files selected for processing (1)
  • docs/performance.md

Comment thread docs/performance.md
…ps asking

Review round two on #228. The request was to file the target-cpu=native A-B-A in
docs/accuracy-ledger.md. Declined -- for the third time in this series, which is
the actual problem and is what this commit fixes.

The ledger states its own scope: "Three things, and nothing else" -- constants the
hardware documentation does not supply, known-wrong emulated behavior, and
ruled-out approaches. All three are about the emulated machine. A compiler flag
that changed no emulated behavior is none of them, and filing it there would blur
what the file is for.

The underlying concern is right, though: the evidence must be recorded in a
ruled-out list with its method. It already is -- docs/performance.md carries its
own "Ruled out by measurement -- do not retry" list, and target-cpu=native is item
5b with the benchmark, the A-B-A table, the drift interpretation, the portability
qualification and the PGO caveat. The finding was in a ruled-out list all along;
only the file was in question.

So rather than reject a third time and wait for a fourth, the ledger now states
the boundary explicitly and points at docs/performance.md. The boundary was
previously discoverable only by reading both files and noticing that one of them
explains the split -- which is not discoverable at all.

Gates: fmt, clippy -D warnings, test --workspace, rustdoc -D warnings,
check_en_us.sh, markdownlint -- ALL-GATES-OK.
@doublegate

Copy link
Copy Markdown
Owner Author

Round two. Rejected as a move — adopted as a discoverability fix, because this is the third time the same request has come and that is the real signal.

Why not the ledger

docs/accuracy-ledger.md states its own scope in its own words: "Three things, and nothing else"

  1. measured constants the hardware documentation does not supply,
  2. known-wrong emulated behavior,
  3. ruled-out approaches.

All three are about the emulated machine. -C target-cpu=native changed no emulated behavior whatsoever — it is a host code-generation flag, and the emulator computes byte-identical results with and without it. Filing it there would blur exactly what that file exists to keep sharp, and the file says so itself: "an entry here is honest, a per-quirk patch is not."

Why the underlying concern is right anyway

The substance of the request — this must be recorded in a ruled-out list, with benchmark, method, result, drift interpretation, and limitations — is correct, and it already is.

docs/performance.md carries its own "Ruled out by measurement — do not retry" list. target-cpu=native is item 5b, and the section it points to has the benchmark, the full A-B-A table, the drift interpretation (the third baseline leg landing below both native legs), the portability qualification adopted in round one, and the PGO caveat.

So the finding was in a ruled-out list the whole time. Only the file was ever in question.

What I changed instead

Rejecting a third time and waiting for a fourth would be the wrong response to a repeated signal. The ledger now states the boundary explicitly and points at docs/performance.md:

Host-performance figures do not belong here — frame times, FPS, profile shares, and ruled-out optimizations live in docs/performance.md […] Stated explicitly because review has asked for the move three times, which is a sign the boundary was discoverable only by reading both files.

Previously the split was explained only inside performance.md, which meant you had to already know where to look to find out where to look.

Gates: fmt, clippy -D warnings, test --workspace, rustdoc -D warnings, check_en_us.sh, markdownlint. ALL-GATES-OK.

@doublegate
doublegate merged commit fc3338a into main Jul 31, 2026
1 check passed
@doublegate
doublegate deleted the docs/target-cpu-native-is-neutral branch July 31, 2026 00:48
@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This PR documents performance benchmark results showing -C target-cpu=native is performance-neutral on the target benchmark and clarifies the organizational boundaries between docs/accuracy-ledger.md and docs/performance.md.

Blocking issues

None found.

Suggestions

  • docs/accuracy-ledger.md:23-25: Remove meta-commentary about past code reviews ("Stated explicitly because review has asked for the move three times..."). Project documentation should state architectural and process boundaries objectively without referencing PR discussion history.
  • docs/performance.md:629: Item 5b. is placed above item 5.. Renumber or resequence the list items to maintain correct numerical order.

Nitpicks

  • docs/performance.md:847: The calculation 0.9995x claims neutrality based on mean point values without stating the benchmark's margin of error or sample variance across runs.

Automated first-pass review by agy on a self-hosted runner -- not a human review.

doublegate added a commit that referenced this pull request Jul 31, 2026
…easure it

PGO was the last untested codegen lever, named in #228 as the most promising one
because interpreters are the workload it classically helps most. It was tested. It
is a 4.96% regression.

FIRST IT NEEDED A HARNESS, and that is a finding in itself. frame_cost_probe is a
#[test], and a test cannot be built with -Cprofile-use in this workspace: tests
require panic = "unwind" while the release profile is panic = "abort", and
RUSTFLAGS applies the flag to every crate in the graph, so they collide with
"requires panic strategy abort which is incompatible with this crate's strategy of
unwind". That survives cargo clean --release, so it is structural rather than
stale artifacts, and the same command with RUSTFLAGS unset builds and runs fine.
The binary path has no such requirement, so this adds
crates/rustyn64-frontend/examples/frame_bench.rs -- the same measurement shaped so
it can be instrumented and rebuilt. An example rather than a [[bin]] so a plain
cargo build does not carry a benchmark into every developer's build.

Full cycle: instrument, run, llvm-profdata merge (18.6 MB from 50 raw files),
rebuild with -Cprofile-use, A-B-A in one sitting over 120 frames of Super Mario 64:

  A  baseline          98.323 ms
  B  PGO              104.060 / 102.918 ms
  A  baseline, again   98.764 / 98.704 ms

Mean A 98.597, mean B 103.489 -- PGO IS 4.96% SLOWER. The worst baseline leg is
still faster than the best PGO leg, so the legs do not overlap and this is not the
drift that caught target-cpu=native in #228. Two integrity checks: the instrumented
run cost 141.6 ms/frame, the expected ~44% overhead, and all three builds retired
an identical 219,075,686 instructions, so they executed exactly the same work.

WHY is inference, not measurement, and is labelled as such: this workspace already
builds with lto = "fat" and codegen-units = 1, giving LLVM whole-program
visibility, and PGO's profile-driven inlining and block layout override choices
fat LTO had already made globally. Consistent with the result and with PGO's usual
caveat about aggressive LTO, but nothing here isolates it -- confirming it needs a
comparison at thin or no LTO. Recorded as a hypothesis so it is not later cited as
a finding.

Not adopted; the ruled-out list gains item 5c. What this does NOT say: PGO is not
useless in general, and a different build configuration could change the answer.
It says PGO is a regression for THIS configuration on THIS workload, which is all
three paired runs can support.

Review improved the harness in two ways. The completion witness counted cumulative
retired instructions, which include the 36 warm-up frames -- a timed loop that
stalled entirely would still have shown ~47M and reported a very fast frame, so
the witness would have been witnessing the warm-up. It is now a delta across the
timed window (171,471,972). And the scan-out's presence in the timed loop is now
stated where the question arises: run_frame is run_until + produce_frame (which
calls scanout_scaled) + produce_audio, so presentation is inside every timed
frame, while the warm-up's extra call is only the VI-liveness probe.

The A-B-A legs are marked as taken in one sitting: a verification run of the same
binary an hour later, after heavy building, read 101.6 ms. That is the drift the
protocol controls for, and the return legs are why the comparison holds -- but the
legs are only comparable to each other.

With this, every reachable codegen lever is measured: instruction selection
neutral, PGO negative, and the fast-scheduler's 1.054x already merged.

Refs #55.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant