perf(react_compiler): use IndexVec for dense id-keyed maps#24549
Merged
Conversation
Merging this PR will not alter performance
Comparing Footnotes
|
Member
Author
Merge activity
|
Each distinct `FxHashMap<K, V>` monomorphizes its own copy of hashbrown; maps keyed by the dense u32 newtype ids become direct-indexed `IndexVec<Id, Option<V>>`s, removing those instantiations and the per-lookup hashing.
### Migration criteria
A map was migrated only when all of these hold:
1. The key is a dense id (`IdentifierId`, `DeclarationId`, `BlockId`) with a sound bound available at construction time: `env.identifiers.len()` (declaration ids share the identifier id index space — `DeclarationId::from_usize(id.index())` at allocation), `env.next_block_id_counter` (+1 for the synthetic exit node in the dominator), or the max block id present in the function body.
2. The map is only accessed by key (`get`/`insert`/`contains_key`); it is never iterated in a way that can reach the output. The one iterated map (the optional-chain temporaries merge) writes into another map with distinct keys, so iteration order is not observable.
3. `Option<V>` slots preserve the exact presence semantics, including "insert twice keeps latest" and `entry().or_insert()` first-wins. Where `map.len()` carried meaning, it was replaced with an explicit counter (`Rewrites::len` for the phi-elimination fixpoint, `BlockIdMappings::next` for sequential id assignment).
4. The map is allocated once per pass, not per inner loop iteration.
### Migrated maps
| File | Map |
| --- | --- |
| `react_compiler_hir/dominator.rs` | `PostDominator::nodes`, `Graph::node_index`, `raw_nodes`, `doms` (kills `FxHashMap<BlockId, BlockId>`, `<BlockId, Node>`, `<BlockId, usize>`) |
| `react_compiler_reactive_scopes/stabilize_block_ids.rs` | block id `mappings` (new `BlockIdMappings` with explicit `next` counter) |
| `react_compiler_optimization/prune_maybe_throws.rs` | `terminal_mapping` |
| `react_compiler_ssa/eliminate_redundant_phi.rs` | `rewrites` (new `Rewrites` with explicit entry count for the fixpoint check) |
| `react_compiler_validation/validate_hooks_usage.rs` | `value_kinds` (`Option<Kind>` is 1 byte) |
| `react_compiler_reactive_scopes/promote_used_temporaries.rs` | `State::pruned`, `inter_state` |
| `react_compiler_reactive_scopes/merge_reactive_scopes_that_invalidate_together.rs` | `last_usage`, `temporaries` |
| `react_compiler_inference/propagate_scope_dependencies_hir.rs` | `temporaries` + `temporaries_read_in_optional` (the crate's most-instantiated map type, ×11), the declaring-scope map in `find_temporaries_used_outside_declaring_scope`, and `DependencyCollectionContext::{declarations, reassignments}` |
This eliminates 11 distinct id-keyed hashbrown instantiations from the crate.
### Deliberately skipped
- `outline_jsx.rs` `globals` and `infer_types.rs` / `name_anonymous_functions.rs` `names`: these create a fresh map per nested function, so a dense vec sized by the env-wide identifier count would allocate O(identifiers) for a handful of entries on every nested function.
- `outline_jsx.rs` `rewrite_instr` and other `EvaluationOrder`-keyed maps: no clean bound at the construction site and keys are sparse.
- `get_assumed_invoked_functions` temporaries in `propagate_scope_dependencies_hir.rs`: allocated per inner function (same per-iteration concern).
- `FxIndexMap` uses: insertion order is semantic there.
- Maps whose iteration feeds diagnostics or output ordering (e.g. `validate_no_derived_computations_in_effects.rs`).
### Measured effect
Release example binary (`examples/react_compiler`, macOS arm64): `__text` (machine code) shrinks 2,767,800 → 2,756,116 bytes (**−11,684 bytes, −0.42%**). The on-disk file size only changes by 16 bytes because the `__TEXT` segment is page-aligned; the machine-code section is the meaningful metric. The example links the full parser/codegen stack, so the relative saving within this crate's own code is larger. The main win is runtime: every migrated lookup is now a bounds-checked array index instead of a hash + probe.
The upstream fixture corpus (1,736 snapshots) is byte-for-byte unchanged, and `cargo clippy -p oxc_react_compiler --all-targets` is clean.
### Note for upstream re-syncs
This intentionally deviates from the upstream TypeScript structure (`Map`-based sidemaps in `PropagateScopeDependenciesHIR.ts`, `Dominator.ts`, etc.). When re-syncing this crate with upstream via `react_compiler_oxc`, please preserve the `IndexVec` representation for these maps.
Authored with Claude Code; reviewed by a human before merge.
graphite-app
Bot
force-pushed
the
react-compiler-index-vec
branch
from
July 15, 2026 12:09
aa7d19c to
ba35a0d
Compare
camc314
added a commit
that referenced
this pull request
Jul 21, 2026
### 💥 BREAKING CHANGES - 54cc121 ast: [**BREAKING**] Split `MetaProperty` into `ImportMeta` and `NewTarget` (#24557) (camc314) ### 🚀 Features - 4c71560 parser: More friendly error for spread element in dynamic imports (#24705) (sapphi-red) - 7b045cd minfier: Drop last break from last switch case (#24673) (Armano) - 7d3c178 minifier: Remove unreachable recursive functions (#24125) (Dunqing) - 94f99b3 ast: Allow `NONE` to be passed to AST builder methods where `Option<ArenaVec>` is expected (#24629) (overlookmotel) - 77230c5 ast: Accept arrays for `ArenaVec` params of AST builder methods (#24621) (overlookmotel) - f08b152 allocator: Implement `FromIn` for array to `Vec` conversion (#24620) (overlookmotel) - 2338c13 track-memory-allocations: Track heap deallocs, alloc bytes, and peak growth (#24619) (Boshen) - 7aa4739 syntax,transformer: Move JSX entity decoder to `oxc_syntax` (#24617) (camc314) - 2b097c4 str: Export `Str` as `ArenaStr` (#24604) (overlookmotel) - 3acf4c1 minifier: Expand switch optimiation to remove empty cases (#24520) (Armano) - 129b759 parser: Improve diagnostics for unparenthesized LHS on exponential expr (#24569) (camc314) - 4d0c601 minifier: Fold arithmetic over undefined and null operands (#24485) (Dunqing) - 91541dd minifier: Drop empty switch statements (#24527) (Armano) - d05224d ast_visit: Generate VisitJs visitor that skips TypeScript type-space (#24499) (Boshen) - 3d22307 parser: Add `ParseOptions::enable_ident_hashes` (#24491) (Boshen) ### 🐛 Bug Fixes - 64c2241 minifier: Align class heritage removal with assumptions (#24533) (Dunqing) - 48b59f4 parser: Span ambient generator diagnostics (#24711) (camc314) - e750a82 ecmascript: Fix false negative for may_have_side_effects on dynamic property access (#24709) (sapphi-red) - f145d73 minifier: Guard reordered identifier reads (#24698) (Dunqing) - a2ef382 isolated-declarations: Reject `window.Symbol` as global symbol reference (#24689) (camc314) - b1bcf72 minifier: Invalidate facts for redeclared bindings (#24658) (Dunqing) - 921b834 minifier: Don't treat a conditionally-assigned var as write-once (#24650) (Dunqing) - 061af1f minifier: Avoid stale pure function summaries (#24636) (Dunqing) - 40c2f43 allocator: `Vec::from_array_in` do not allocate zero-length array (#24628) (overlookmotel) - 70994ae codegen: Preserve comments before expression operands (#24510) (Dunqing) - 7b4baff parser: Reject new import member access (#23459) (camc314) - 128b385 minifier: Clippy warning with no-debug-assertions (#24547) (camc314) - 8421feb parser: Use first `as` span for imported name (#24537) (leaysgur) - c517aa0 parser: Reject invalid accessor assertions (#24504) (camc314) ### ⚡ Performance - 884d9eb parser: Pre-size cover-grammar assignment target buffers (#24683) (Boshen) - d3f07a0 diagnostics: Box OxcDiagnosticInner to reduce binary size (#24665) (Boshen) - bcc9de0 parser: Defer diagnostic creation until parse exit (#24663) (Boshen) - c35d8ab allocator: Mark `ReplaceWith` panic path cold (#24515) (camc314) - ba65790 semantic, allocator: Branchless `clone_in` for semantic IDs (#24564) (overlookmotel) - 747feec parser: Build AST nodes with the AST builder instead of cloning (#24540) (Boshen) - ba35a0d react_compiler: Use IndexVec for dense id-keyed maps (#24549) (Boshen) - b685062 react_compiler: Keep small hot-path collections inline (#24514) (Marius Schulz) - a149e95 transformer: Outline rare expression exits (#24512) (camc314) - 7808a6e react_compiler: Make aliasing effects cheap to intern and clone (#24506) (Marius Schulz) - 3a36f2a react_compiler: Store AbstractValue reasons as a u16 bitmask (#24480) (Boshen) - 1c96753 react_compiler: Use FxHashMap for the lookup-only aliasing node map (#24490) (Boshen) Co-authored-by: Cameron <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Each distinct
FxHashMap<K, V>monomorphizes its own copy of hashbrown; maps keyed by the dense u32 newtype ids become direct-indexedIndexVec<Id, Option<V>>s, removing those instantiations and the per-lookup hashing.Migration criteria
A map was migrated only when all of these hold:
IdentifierId,DeclarationId,BlockId) with a sound bound available at construction time:env.identifiers.len()(declaration ids share the identifier id index space —DeclarationId::from_usize(id.index())at allocation),env.next_block_id_counter(+1 for the synthetic exit node in the dominator), or the max block id present in the function body.get/insert/contains_key); it is never iterated in a way that can reach the output. The one iterated map (the optional-chain temporaries merge) writes into another map with distinct keys, so iteration order is not observable.Option<V>slots preserve the exact presence semantics, including "insert twice keeps latest" andentry().or_insert()first-wins. Wheremap.len()carried meaning, it was replaced with an explicit counter (Rewrites::lenfor the phi-elimination fixpoint,BlockIdMappings::nextfor sequential id assignment).Migrated maps
react_compiler_hir/dominator.rsPostDominator::nodes,Graph::node_index,raw_nodes,doms(killsFxHashMap<BlockId, BlockId>,<BlockId, Node>,<BlockId, usize>)react_compiler_reactive_scopes/stabilize_block_ids.rsmappings(newBlockIdMappingswith explicitnextcounter)react_compiler_optimization/prune_maybe_throws.rsterminal_mappingreact_compiler_ssa/eliminate_redundant_phi.rsrewrites(newRewriteswith explicit entry count for the fixpoint check)react_compiler_validation/validate_hooks_usage.rsvalue_kinds(Option<Kind>is 1 byte)react_compiler_reactive_scopes/promote_used_temporaries.rsState::pruned,inter_statereact_compiler_reactive_scopes/merge_reactive_scopes_that_invalidate_together.rslast_usage,temporariesreact_compiler_inference/propagate_scope_dependencies_hir.rstemporaries+temporaries_read_in_optional(the crate's most-instantiated map type, ×11), the declaring-scope map infind_temporaries_used_outside_declaring_scope, andDependencyCollectionContext::{declarations, reassignments}This eliminates 11 distinct id-keyed hashbrown instantiations from the crate.
Deliberately skipped
outline_jsx.rsglobalsandinfer_types.rs/name_anonymous_functions.rsnames: these create a fresh map per nested function, so a dense vec sized by the env-wide identifier count would allocate O(identifiers) for a handful of entries on every nested function.outline_jsx.rsrewrite_instrand otherEvaluationOrder-keyed maps: no clean bound at the construction site and keys are sparse.get_assumed_invoked_functionstemporaries inpropagate_scope_dependencies_hir.rs: allocated per inner function (same per-iteration concern).FxIndexMapuses: insertion order is semantic there.validate_no_derived_computations_in_effects.rs).Measured effect
Release example binary (
examples/react_compiler, macOS arm64):__text(machine code) shrinks 2,767,800 → 2,756,116 bytes (−11,684 bytes, −0.42%). The on-disk file size only changes by 16 bytes because the__TEXTsegment is page-aligned; the machine-code section is the meaningful metric. The example links the full parser/codegen stack, so the relative saving within this crate's own code is larger. The main win is runtime: every migrated lookup is now a bounds-checked array index instead of a hash + probe.The upstream fixture corpus (1,736 snapshots) is byte-for-byte unchanged, and
cargo clippy -p oxc_react_compiler --all-targetsis clean.Note for upstream re-syncs
This intentionally deviates from the upstream TypeScript structure (
Map-based sidemaps inPropagateScopeDependenciesHIR.ts,Dominator.ts, etc.). When re-syncing this crate with upstream viareact_compiler_oxc, please preserve theIndexVecrepresentation for these maps.Authored with Claude Code; reviewed by a human before merge.