Skip to content

perf(react_compiler): avoid copying the aliasing fixpoint's abstract state#24117

Merged
Boshen merged 4 commits into
oxc-project:mainfrom
mds-ant:perf/react-compiler-aliasing-state
Jul 13, 2026
Merged

perf(react_compiler): avoid copying the aliasing fixpoint's abstract state#24117
Boshen merged 4 commits into
oxc-project:mainfrom
mds-ant:perf/react-compiler-aliasing-state

Conversation

@mds-ant

@mds-ant mds-ant commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

infer_mutation_aliasing_effects runs a fixpoint that tracks one InferenceState per block. Each state consists of two hash maps holding every value and variable of the function, and it was deep-cloned per block visit, per successor, per merge, and per variable set. Profiling an Oxlint run with react/react-compiler as the only enabled rule showed this state churn dominating the rule's cost.

Change

This PR makes several optimizations, broken out into separate commits. I'd recommend reviewing these one commit at a time:

  • Avoid redundant state clones: move the incoming state into the working state, hand the post-block state to the last successor by move (most blocks have exactly one successor), and stop replacing a queued state with a clone of itself when a merge is a no-op.
  • Skip state snapshots for acyclic CFGs: states_by_block is only read when a block is re-queued after processing, which requires a CFG cycle; detect back edges up front and skip the per-block snapshot for cycle-free functions (the common case for components).
  • Merge queued states in place: new merge_from applies merge's per-entry updates directly to the queued state instead of building and dropping a full replacement state.
  • Share variable sets via Rc: the variables map's sets are never mutated in place — every write replaces the whole entry — so cloning a state or assigning a variable now bumps a refcount instead of copying a hash table.

Impact

cal.com corpus (5,054 files), react/react-compiler only, 1 thread, release profile (fat LTO) with mimalloc:

Metric baseline this PR Δ
instructions 33.37 B 19.36 B −42.0%
wall time 4.46 s 3.03 s −32.1%

Verification

  • Byte-identical diagnostics: 4/4 diffs clean over cal.com and excalidraw.
  • Transform differential: 1,438/1,438 corpus tsx/jsx files produce identical output (stdout+stderr+exit) through the react_compiler example.
  • cargo test -p oxc_react_compiler (16/16, including the fixture snapshot corpus, whose snapshots were generated from unmodified code), cargo clippy -p oxc_react_compiler, cargo fmt -- --check: all clean.

This PR was assisted by Claude Code.

@codspeed-hq

codspeed-hq Bot commented Jul 3, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 18.72%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 2 improved benchmarks
✅ 8 untouched benchmarks
⏩ 61 skipped benchmarks1

Performance Changes

Mode Benchmark BASE HEAD Efficiency
Simulation react_compiler[kitchen-sink.tsx] 267.3 ms 216.6 ms +23.42%
Simulation linter[kitchen-sink.tsx] 411.9 ms 360.7 ms +14.19%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing mds-ant:perf/react-compiler-aliasing-state (91a74ce) with main (6147d22)

Open in CodSpeed

Footnotes

  1. 61 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@Boshen Boshen self-assigned this Jul 3, 2026
@mds-ant
mds-ant force-pushed the perf/react-compiler-aliasing-state branch from e98a351 to 23c36ba Compare July 6, 2026 08:03
@mds-ant
mds-ant force-pushed the perf/react-compiler-aliasing-state branch from 23c36ba to f5608a3 Compare July 13, 2026 09:49
@mds-ant

mds-ant commented Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Would love to get your eyes on this, @Boshen! I have two more optimizations in the works that further improve performance for the React Compiler lint rules.

@Boshen

Boshen commented Jul 13, 2026

Copy link
Copy Markdown
Member

Would love to get your eyes on this, @Boshen! I have two more optimizations in the works that further improve performance for the React Compiler lint rules.

I'll come back to this once I'm done with my current round of refactors and optimizations.

mds-ant added 4 commits July 13, 2026 18:39
…xpoint

`infer_mutation_aliasing_effects` deep-cloned its `InferenceState` (two
hash maps holding every value and variable of the function) three times
per block visit where one clone suffices:

- the working state was cloned from the incoming state even though the
  incoming state is owned and can be moved;
- every successor received its own clone of the post-block state; the
  last successor can take it by move (most blocks have exactly one
  successor, making this the common case);
- when a queued state already subsumed a newly merged-in state
  (`merge` returned `None`), the queued entry was replaced by a clone
  of itself instead of being left untouched.

State contents and processing order are unchanged.
The aliasing fixpoint stores a snapshot of every block's incoming
`InferenceState` in `states_by_block`, but that map is only ever read
when a block is queued again after having been processed — which can
only happen when the CFG has a cycle. HIR blocks are stored in reverse
postorder, so the successor graph is acyclic exactly when every edge
points to a strictly later block in the map.

Detect back edges up front and skip the snapshot bookkeeping entirely
for cycle-free functions (the common case for React components), saving
one full state clone per block visit plus the cost of dropping the map.
Functions with loops keep the exact previous behavior.
When a block is queued while already having a queued state, `merge`
built a complete replacement state — cloning both hash maps wholesale
even when only a handful of entries changed — and the old entry was
dropped. Apply the same per-entry updates directly to the queued state
instead.

Since `FxHashMap::clone` preserves table layout, updating in place
yields exactly the same contents and iteration order as replacing the
entry with `merge`'s result, so downstream behavior (including
insertion-ordered reason sets feeding diagnostics) is unchanged.
`InferenceState.variables` mapped each identifier to an owned
`FxHashSet<ValueId>`, so every state clone (branch successors, back-edge
snapshots, merge results) deep-copied every per-variable set, and every
`Assign` effect deep-copied the source set. All writes to the map
replace whole entries — the sets are never mutated in place — so the
sets can be shared: cloning a state or assigning a variable now bumps a
refcount instead of copying a hash table.

Readers iterate the same physical table a layout-preserving deep clone
would have produced, so iteration order (which feeds reason-set order
and diagnostic wording) is unchanged by construction; freshly built
union/phi/singleton sets are constructed exactly as before.
@Boshen
Boshen force-pushed the perf/react-compiler-aliasing-state branch from f5608a3 to 91a74ce Compare July 13, 2026 10:41
@Boshen
Boshen merged commit e86e0d8 into oxc-project:main Jul 13, 2026
30 checks passed
Boshen pushed a commit that referenced this pull request Jul 14, 2026
…#24506)

## What

The mutation/aliasing inference interns every effect it applies. Both
the interning map and the fixpoint-stable ValueId cache were keyed by a
formatted `String`, so every lookup on the `apply_effect` hot path built
a fresh string. On top of that, `AliasingEffect::Apply` carried an owned
`FunctionSignature`, so cloning an Apply effect meant copying the whole
struct including its parameter effect list, and `apply_signature`
deep-cloned the cached effect list of every instruction it applied.

## Change

- Replace the `String` key with an `EffectKey` enum carrying exactly the
fields the string encoded. `Apply::signature`/`span` and
`Mutate::reason` still don't participate in identity, and the one
collision the string format had (an empty `Apply` argument list vs a
single hole) is reproduced on purpose. Building and hashing a key no
longer allocates or formats, except in the rare error-carrying arms,
which clone their message strings.
- Put the `FunctionSignature` in `Apply` behind `Rc`. It's still
deep-cloned out of the shape registry once per call instruction, but
every later clone of the effect is just a refcount bump. The interning
key already ignores the signature, and all readers are immutable.
- Put the cached `InstructionSignature` entries behind `Rc` too, so
`apply_signature` can hold the signature while passing the context on
mutably, instead of cloning the whole effect list each time.

Follow-up to #24117; this PR is based on current main, which includes
it.

Numbers from linting cal.com with only `react/react-compiler` enabled,
single thread, fat-LTO release build:

| Metric (cal.com, 1 thread) |     main |  this PR | delta |
| -------------------------- | -------: | -------: | ----: |
| instructions               | 19.113 G | 18.086 G | −5.4% |
| wall time                  |  2.907 s |  2.804 s | −3.5% |

## Verification

- Output of the main binary vs this PR's binary is byte-identical
(stdout, stderr, exit code) on all 4 corpus/config combinations: cal.com
and excalidraw, each with `reportAllBailouts` on and off.
- The `react_compiler` example (parse, compile, codegen) produces
identical output on all 1438 corpus files.
- `cargo test -p oxc_react_compiler` passes, including the 1,736-fixture
snapshot corpus: compiled output is unchanged on every fixture.
- `cargo clippy` and `cargo fmt` clean.

One note for future re-syncs with the vendored React Compiler sources:
`EffectKey` mirrors `AliasingEffect` variant for variant. Adding an
identity-relevant field to a variant without updating `effect_key`
silently changes interning behavior. Keep the two in sync.

This PR was assisted by Claude Code.
Boshen pushed a commit that referenced this pull request Jul 22, 2026
…ate merge (#24805)

## Context

Value sets in the aliasing inference state are `Rc<FxHashSet<ValueId>>`,
shared between states rather than copied (#24117). When two states meet
at a join, the merge still walks every variable and probes one set
against the other looking for a value the current state doesn't have.
Most of the time both sides point at the same `Rc`, so that probe is
comparing a set to itself.

## This PR

Check `Rc::ptr_eq` before probing. If the pointers match there is
nothing to add, so we skip the scan. Both merge paths did this check in
slightly different ways, so I pulled it into one helper. The sets are
never mutated in place, which is what makes the pointer check safe.

### Performance

| workload | main | this PR | delta |
| ----------------------------------------------------- | -------: |
-------: | ------: |
| 3.6k-line control-flow heavy component, instructions | 23.762 B |
22.034 B | -7.3% |
| same file, wall time |   4.55 s |   4.13 s |     -9% |
| 16.5k-file real corpus, instructions (1 thread) | 50.135 B | 49.697 B
| -0.87% |

This PR was assisted by Claude Code.
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.

2 participants