perf(react_compiler): avoid copying the aliasing fixpoint's abstract state#24117
Conversation
Merging this PR will improve performance by 18.72%
|
| 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)
Footnotes
-
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. ↩
e98a351 to
23c36ba
Compare
23c36ba to
f5608a3
Compare
|
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. |
…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.
f5608a3 to
91a74ce
Compare
…#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.
…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.
What
infer_mutation_aliasing_effectsruns a fixpoint that tracks oneInferenceStateper 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 withreact/react-compileras 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:
states_by_blockis 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_fromappliesmerge's per-entry updates directly to the queued state instead of building and dropping a full replacement state.Rc: thevariablesmap'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-compileronly, 1 thread, release profile (fat LTO) with mimalloc:Verification
react_compilerexample.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.