Skip to content

docs(perf): the VU family hoist — built, accurate, and measured neutral - #239

Merged
doublegate merged 2 commits into
mainfrom
perf/rsp-vu-family-hoist
Jul 31, 2026
Merged

docs(perf): the VU family hoist — built, accurate, and measured neutral#239
doublegate merged 2 commits into
mainfrom
perf/rsp-vu-family-hoist

Conversation

@doublegate

Copy link
Copy Markdown
Owner

Result: built, accurate, neutral — reverted

The single-family hoist experiment, run against the RSP gate #238 added for it. Docs-only; the hoist itself is not landed.

Which family — measured, not guessed

An opcode histogram in Rsp::vu_compute over 900 frames of Super Mario 64: 125 M VU compute ops, ~139 k per frame.

op share
0x0e 17.8% multiply
0x0f 15.2% multiply
0x0d 8.7% multiply
0x10 8.7% VADD
0x04 7.9% multiply
0x00 6.0% multiply

The multiply family (0x00..=0x0F) is ~61% of all VU compute ops — and it is the only family that dispatched twice per lane (outer match, then multiply_lane's own), so the opcode was decoded sixteen times per instruction. If dispatch-in-the-loop were the constraint, this is where it would show.

The change was correct

A dedicated eight-lane loop for op <= 0x0F, returning directly — the multiplies write no VCC, no VCO, and fall outside the 0x28..=0x2D accumulator range, so none of the shared tail applies.

Accuracy held, on the gate built for exactly this:

RSP categories: 0 failing across 224 RSP tests started
(suite ran to xioctl(EXIT); 90 failing suite-wide across 950 started).

And it is worth nothing

Six readings a side, frame_bench, one sitting:

leg ms/frame mean
A (baseline) 64.114, 64.303, 64.183, 64.110, 63.744, 63.899 64.059
B (hoisted) 63.840, 64.182, 64.040, 63.473, 63.775, 63.517 63.804

Ranges: A 63.744–64.303, B 63.473–64.182heavy overlap. The means differ by 0.40% in the hoist's favor; the conservative pairing (best A against worst B) says it is 0.68% slower.

When the sign of the result depends on which pairing you quote, the result is neutral. Same standard as the rsp_tick early-out, same conclusion. Reverted per this document's standing rule — revert anything neutral or worse, as was done for the next_edge hoist, the vi_divot reorder, target-cpu=native and PGO.

What it settles — the reason this was worth doing

This was the strongest available test of "the per-lane dispatch is what stops the VU going fast": run on the family that is 61% of the work and carries double the dispatch. It moved nothing.

Taken with the decode sizing (0.29% for a perfect decode cache):

The VU's 8.54% of a frame is the arithmetic itself, not the dispatch around it. No amount of restructuring the interpreter reaches it.

What remains is genuine SIMD — eight u16 lanes per operation, which is what the hardware is — or nothing.

That makes the unsafe/nightly decision the real gate on further VU work, rather than something to defer behind a restructure. This measurement doesn't make that call; it removes the cheaper alternative that was standing in front of it.

Ruled out

Do not hoist VU opcode families out of the lane loop for performance. Measured neutral on the largest family, with accuracy verified. The remaining families are smaller and singly-dispatched, so they can only measure smaller.

Verification

bash scripts/check_en_us.sh; pre-commit run markdownlint --all-files. All instrumentation was scratch — vu.rs snapshotted, restored, and the tree verified clean against the snapshot between every leg.

🤖 Generated with Claude Code

The experiment the SIMD section asked for, run against the RSP gate added in
#238. It reverts under this document's own rule, and what it settles is worth
more than the change would have been.

WHICH FAMILY, MEASURED RATHER THAN GUESSED. An opcode histogram in vu_compute
over 900 frames of Super Mario 64: 125 M VU compute ops, ~139 k per frame. The
MULTIPLY FAMILY (0x00..=0x0F) is ~61% of them — 0x0e alone is 17.8%, 0x0f 15.2%.
It is also the only family that dispatched TWICE per lane (outer match, then
multiply_lane's own), so the opcode was decoded sixteen times per instruction.
If dispatch-in-the-loop were the constraint, that is where it would show.

THE CHANGE WAS CORRECT. A dedicated eight-lane loop for op <= 0x0F, returning
directly since the multiplies write no VCC, no VCO, and fall outside the
0x28..=0x2D accumulator range. Accuracy held on the new gate: RSP 0 failing
across 224 tests started, suite-wide unchanged at 90.

AND IT IS WORTH NOTHING. Six readings a side, one sitting:

  A  64.114 64.303 64.183 64.110 63.744 63.899   (mean 64.059)
  B  63.840 64.182 64.040 63.473 63.775 63.517   (mean 63.804)

The ranges overlap heavily. Means differ by 0.40% in the hoist's favour while
the conservative pairing says it is 0.68% SLOWER. When the sign depends on which
pairing you quote, the result is neutral — the same standard applied to the
rsp_tick early-out, and the same conclusion. Reverted per the standing rule.

WHAT IT SETTLES. This was the strongest available test of "the per-lane dispatch
is what stops the VU going fast", on the family that is 61% of the work and
carries double the dispatch. It moved nothing.

With the decode sizing (0.29% for a perfect cache), the VU's 8.54% of a frame is
THE ARITHMETIC ITSELF, not the dispatch around it. No amount of restructuring the
interpreter reaches it. What remains is genuine SIMD — eight u16 lanes per
operation, which is what the hardware is — or nothing.

That makes the unsafe/nightly decision the real gate on further VU work rather
than something to defer behind a restructure. This measurement does not make that
call; it removes the cheaper alternative that was standing in front of it.

Recorded under Ruled out: do not hoist VU opcode families for performance. The
remaining families are smaller and singly-dispatched, so they can only measure
smaller.

All instrumentation was scratch: vu.rs snapshotted, restored, tree verified clean
against the snapshot between every leg.

Gates: check_en_us.sh, markdownlint. Docs-only — the hoist itself is not landed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@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: 3 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: f7080aff-56c5-4e44-a3c4-bd41c4c244fa

📥 Commits

Reviewing files that changed from the base of the PR and between 6d77c8d and 1e945bc.

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

Summary by CodeRabbit

  • Bug Fixes

    • Confirmed accurate RSP behaviour across 224 tests.
    • Preserved shared-state behaviour for vector multiply operations.
  • Performance

    • Evaluated an optimisation for vector multiply processing.
    • No measurable improvement to frame timings was observed; runtime behaviour remains unchanged.

Walkthrough

The performance documentation records a VU multiply-family dispatch-hoisting experiment, RSP accuracy results, A-B-A timing measurements, and the decision to revert the neutral optimisation.

Changes

VU multiply-family hoist

Layer / File(s) Summary
Experiment and validation
docs/performance.md
The document adds an eight-lane VU multiply-family loop and records successful accuracy testing across 224 started RSP tests.
Measurement and conclusion
docs/performance.md
The document records overlapping baseline and hoisted timings, classifies the result as neutral, and identifies SIMD as the remaining VU optimisation direction.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 8 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title uses a valid Conventional Commits prefix and length, but its subject is descriptive rather than imperative. Rewrite the subject in the imperative mood while preserving the docs scope and the measured neutral result.
Measured, Never Tuned ⚠️ Warning docs/performance.md:1633-1645 adds 63.473–64.303 ms/frame and 0.40%/0.68% timing claims, but no manual/wiki citation or matching accuracy-ledger entry exists. Record the new timing measurements and method in docs/accuracy-ledger.md, or remove the uncited quantitative claims.
✅ Passed checks (8 passed)
Check name Status Explanation
Description check ✅ Passed The description directly documents the reverted VU hoisting experiment, its accuracy results, benchmark results, and verification.
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: the pull request changes only docs/performance.md; the custom rule explicitly passes docs-only changes.
Docs-As-Spec Sync ✅ Passed The commit changes only docs/performance.md; no chip-crate code or observable behaviour changed, so the conditional documentation-sync rule is not triggered.
Changelog Entry For User-Visible Changes ✅ Passed The patch only adds docs/performance.md for a reverted, unlanded experiment and changes no user-visible behaviour; therefore no [Unreleased] entry is required.
Unsafe Stays Out Of The Chip Crates ✅ Passed The PR changes only docs/performance.md; all five chip crates and rustyn64-core retain forbid(unsafe_code), and Rust-source scans found no unsafe syntax.

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 1653-1677: Revise the “What it settles” and “Ruled out”
conclusions to cover only the measured multiply-family dispatch hoist for this
workload. Remove definitive claims that interpreter restructuring cannot help,
SIMD is the only improvement, or other opcode families can only have smaller
effects; label those broader points as unverified hypotheses requiring separate
measurements.
- Around line 1629-1647: Complete the VU-hoist benchmark documentation around
the displayed A/B measurements by recording the actual A–B–A run order, host,
operating system, toolchain, revision, and exact command, or link to an existing
source containing all of that provenance. Preserve the current measurements and
conclusion while satisfying the §Measured requirement for a reproducible method.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 292e3e7d-ce21-49bd-ab74-aa926c004b8d

📥 Commits

Reviewing files that changed from the base of the PR and between 9b61916 and 6d77c8d.

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

Comment thread docs/performance.md
Comment thread docs/performance.md Outdated
Two review findings.

I OVER-GENERALISED FROM ONE FAMILY, AGAIN. The section claimed that no
interpreter restructuring reaches the VU, that SIMD or nothing is what remains,
and that the other families can only measure smaller. None of that was measured.
One family was hoisted on one workload.

Split into what IS established — hoisting the multiply family's dispatch is
neutral on SM64's render phase, and with the 0.29% decode sizing that is two of
the three things a pre-decoded threaded interpreter would do — and what is
HYPOTHESIS, each labelled and each needing its own measurement: that the 8.54% is
predominantly arithmetic (neither experiment decomposed the per-lane body); that
no restructuring helps (a full per-opcode specialisation, which also hoists
multiply_lane's inner match, is a DIFFERENT change); that smaller families
measure smaller (a reason to expect it, not a measurement); and that SIMD is what
remains (obvious candidate, untried).

The Ruled-out entry now rules out REPEATING THIS EXPERIMENT rather than the
different change it was being read to cover.

What survives is narrower and still useful: the cheapest candidate is gone from
the front of the queue, so the unsafe/nightly decision is no longer deferrable
behind "restructure first, it is free" — the free restructure was tried on its
best case and did nothing.

PROVENANCE ADDED for the benchmark: the actual A-B-A order (A x3 -> B x3 -> B x3
-> A x3, the B blocks contiguous because the rebuild is what costs time), the
revision, and the exact command; host, OS, toolchain, profile and ROM hash are
the ones recorded once in section Method rather than duplicated.

Gates: check_en_us.sh, markdownlint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Antigravity review (Gemini via Ultra)

This documentation-only PR records opcode analysis and benchmark results for a vector unit (VU) multiply opcode dispatch hoisting experiment, documenting that the change yielded neutral performance results in Super Mario 64 and justifying its reversion.

Blocking issues

None found.

Suggestions

  • docs/performance.md:1596: The third column header in the opcode distribution table is empty (| op | share | |). Label it explicitly (e.g., | op | share | Category |) to keep table headers well-formed.
  • docs/performance.md:1648: The mean for run B is listed as 63.804, but calculating the arithmetic mean of the six B data points yields 63.8045. Round to 63.805 to match the precision of mean A (64.059).

Nitpicks

  • docs/performance.md:1625: Ensure xioctl(EXIT) is an literal stdout snippet from the RSP test harness rather than a typo.

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

@doublegate
doublegate merged commit 381f2ff into main Jul 31, 2026
12 checks passed
@doublegate
doublegate deleted the perf/rsp-vu-family-hoist branch July 31, 2026 20:51
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