@samitouri / QOS-React / commits / 05769e026b

React Compiler: Make AbstractValue Copy to reduce heap allocation (#37225)

## Summary By replicating the `IndexSet` behavior with a simple inline array, this is able to avoid any heap allocation / memory thrash for `AbstractValue`. It also reduces the size of AbstractValue from 72 bytes + all of the heap allocations, to only 18 bytes. ## How did you test this change? Ran all of the fixtures to confirm byte output is identical. Effects on memory and compile time: | Benchmark | Peak allocation | Allocation count | Wall time | |------------------|-----------------------------|------------------|-----------| | legacy/image.tsx | 58.21 -> 33.40 MiB (-42.6%) | -51.1% | -39.5% | | next-client | 58.21 -> 33.40 (-42.6%) | -40.5% | -25.1% | | devtools | 26.39 -> 16.29 (-38.3%) | -26.7% | -14.8% | | fixtures | 17.35 → 9.41 (-45.8%) | -9.9% | -9.0% |

Andrew Imm committed Aug 24, 2026 at 10:38 UTC 05769e026b9b1c596bbcb99aadd4d4252ccd1dff
1 file changed +109 -46
compiler/crates/react_compiler_inference/src/infer_mutation_aliasing_effects.rs
+109 -46
@@ -11,7 +11,7 @@
11 //! creation, aliasing, mutation, freezing, and error conditions for each
12 //! instruction and terminal in the HIR.
13
14 -use indexmap::{IndexMap, IndexSet};
14 +use indexmap::IndexMap;
15 use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
16
17 use react_compiler_diagnostics::CompilerDiagnostic;
@@ -69,7 +69,7 @@ pub fn infer_mutation_aliasing_effects(
69 value_id,
70 AbstractValue {
71 kind: ValueKind::Context,
72 - reason: hashset_of(ValueReason::Other),
72 + reason: ValueReasonSet::single(ValueReason::Other),
73 },
74 );
75 initial_state.define(ctx_place.identifier, value_id);
@@ -78,12 +78,12 @@ pub fn infer_mutation_aliasing_effects(
78 let param_kind: AbstractValue = if is_function_expression {
79 AbstractValue {
80 kind: ValueKind::Mutable,
81 - reason: hashset_of(ValueReason::Other),
81 + reason: ValueReasonSet::single(ValueReason::Other),
82 }
83 } else {
84 AbstractValue {
85 kind: ValueKind::Frozen,
86 - reason: hashset_of(ValueReason::ReactiveFunctionArgument),
86 + reason: ValueReasonSet::single(ValueReason::ReactiveFunctionArgument),
87 }
88 };
89
@@ -103,7 +103,7 @@ pub fn infer_mutation_aliasing_effects(
103 value_id,
104 AbstractValue {
105 kind: ValueKind::Mutable,
106 - reason: hashset_of(ValueReason::Other),
106 + reason: ValueReasonSet::single(ValueReason::Other),
107 },
108 );
109 initial_state.define(ref_place.identifier, value_id);
@@ -258,16 +258,88 @@ impl ValueId {
258 // AbstractValue
259 // =============================================================================
260
261 -#[derive(Debug, Clone)]
261 +#[derive(Debug, Clone, Copy)]
262 struct AbstractValue {
263 kind: ValueKind,
264 - reason: IndexSet<ValueReason, FxBuildHasher>,
264 + reason: ValueReasonSet,
265 +}
266 +
267 +/// Capacity of [`ValueReasonSet`]. A set holds at most one of each `ValueReason`
268 +/// variant, of which there are currently 12; the extra slots are headroom so
269 +/// that adding variants upstream cannot overflow the set.
270 +const VALUE_REASON_CAPACITY: usize = 16;
271 +
272 +/// An insertion-ordered set of [`ValueReason`]s, stored inline.
273 +///
274 +/// This is a deliberate replacement for `IndexSet`, enabling insertion-order
275 +/// memory while avoiding any heap allocation. At `AbstractValue`'s scale, this
276 +/// has a dramatic impact on heap memory and wall time.
277 +/// This takes advantage of the format of the data it's actually storing. A set
278 +/// can hold at most one of each variant, so the members fit into a fixed inline
279 +/// array. `ValueReason` is implemented as a single byte, so this struct is
280 +/// ~18 bytes on the stack.
281 +///
282 +/// Insertion order is preserved deliberately: [`primary_reason`] returns the
283 +/// first non-`Other` member, matching the iteration order of the `Set` used by
284 +/// the TypeScript implementation this is ported from.
285 +#[derive(Debug, Clone, Copy)]
286 +struct ValueReasonSet {
287 + /// Members in insertion order. Only the first `len` entries are meaningful.
288 + members: [ValueReason; VALUE_REASON_CAPACITY],
289 + len: u8,
290 }
291
267 -fn hashset_of(r: ValueReason) -> IndexSet<ValueReason, FxBuildHasher> {
268 - let mut s = IndexSet::default();
269 - s.insert(r);
270 - s
292 +impl Default for ValueReasonSet {
293 + fn default() -> Self {
294 + ValueReasonSet {
295 + members: [ValueReason::Other; VALUE_REASON_CAPACITY],
296 + len: 0,
297 + }
298 + }
299 +}
300 +
301 +impl ValueReasonSet {
302 + fn single(reason: ValueReason) -> Self {
303 + let mut set = Self::default();
304 + set.insert(reason);
305 + set
306 + }
307 +
308 + fn contains(&self, reason: ValueReason) -> bool {
309 + self.members[..self.len as usize].contains(&reason)
310 + }
311 +
312 + fn iter(&self) -> impl Iterator<Item = ValueReason> + '_ {
313 + self.members[..self.len as usize].iter().copied()
314 + }
315 +
316 + /// Appends `reason` if not already present, preserving insertion order.
317 + fn insert(&mut self, reason: ValueReason) {
318 + if self.contains(reason) {
319 + return;
320 + }
321 + debug_assert!(
322 + (self.len as usize) < VALUE_REASON_CAPACITY,
323 + "ValueReasonSet capacity must cover every ValueReason variant"
324 + );
325 + if (self.len as usize) < VALUE_REASON_CAPACITY {
326 + self.members[self.len as usize] = reason;
327 + self.len += 1;
328 + }
329 + }
330 +
331 + /// True when every member of `other` is also a member of `self`.
332 + fn is_superset_of(&self, other: &ValueReasonSet) -> bool {
333 + other.iter().all(|reason| self.contains(reason))
334 + }
335 +
336 + /// Adds every member of `other`, keeping `self`'s existing order and
337 + /// appending newcomers in `other`'s order — matching `IndexSet::insert`.
338 + fn union_with(&mut self, other: &ValueReasonSet) {
339 + for reason in other.iter() {
340 + self.insert(reason);
341 + }
342 + }
343 }
344
345 // =============================================================================
@@ -315,7 +387,7 @@ impl InferenceState {
387 }
388 return AbstractValue {
389 kind: ValueKind::Mutable,
318 - reason: hashset_of(ValueReason::Other),
390 + reason: ValueReasonSet::single(ValueReason::Other),
391 };
392 }
393 };
@@ -332,7 +404,7 @@ impl InferenceState {
404 }
405 merged_kind.unwrap_or_else(|| AbstractValue {
406 kind: ValueKind::Mutable,
335 - reason: hashset_of(ValueReason::Other),
407 + reason: ValueReasonSet::single(ValueReason::Other),
408 })
409 }
410
@@ -360,7 +432,7 @@ impl InferenceState {
432 vid,
433 AbstractValue {
434 kind: ValueKind::Mutable,
363 - reason: hashset_of(ValueReason::Other),
435 + reason: ValueReasonSet::single(ValueReason::Other),
436 },
437 );
438 }
@@ -438,7 +510,7 @@ impl InferenceState {
510 value_id,
511 AbstractValue {
512 kind: ValueKind::Frozen,
441 - reason: hashset_of(reason),
513 + reason: ValueReasonSet::single(reason),
514 },
515 );
516 // Note: In TS, this also transitively freezes FunctionExpression captures
@@ -493,7 +565,7 @@ impl InferenceState {
565 if let Some(other_value) = other.values.get(id) {
566 let merged = merge_abstract_values(this_value, other_value);
567 if merged.kind != this_value.kind
496 - || !is_superset(&this_value.reason, &merged.reason)
568 + || !this_value.reason.is_superset_of(&merged.reason)
569 {
570 let nv = next_values.get_or_insert_with(|| self.values.clone());
571 nv.insert(*id, merged);
@@ -566,13 +638,6 @@ impl InferenceState {
638 }
639 }
640
569 -fn is_superset(
570 - a: &IndexSet<ValueReason, FxBuildHasher>,
571 - b: &IndexSet<ValueReason, FxBuildHasher>,
572 -) -> bool {
573 - b.iter().all(|x| a.contains(x))
574 -}
575 -
641 #[derive(Debug, Clone, Copy)]
642 enum MutateVariant {
643 Mutate,
@@ -738,13 +803,11 @@ fn hash_effect(effect: &AliasingEffect) -> String {
803
804 fn merge_abstract_values(a: &AbstractValue, b: &AbstractValue) -> AbstractValue {
805 let kind = merge_value_kinds(a.kind, b.kind);
741 - if kind == a.kind && kind == b.kind && is_superset(&a.reason, &b.reason) {
806 + if kind == a.kind && kind == b.kind && a.reason.is_superset_of(&b.reason) {
807 return a.clone();
808 }
744 - let mut reason = a.reason.clone();
745 - for r in &b.reason {
746 - reason.insert(*r);
747 - }
809 + let mut reason = a.reason;
810 + reason.union_with(&b.reason);
811 AbstractValue { kind, reason }
812 }
813
@@ -1233,7 +1296,7 @@ fn apply_signature(
1296 vid,
1297 AbstractValue {
1298 kind: ValueKind::Mutable,
1236 - reason: hashset_of(ValueReason::Other),
1299 + reason: ValueReasonSet::single(ValueReason::Other),
1300 },
1301 );
1302 state.define(instr.lvalue.identifier, vid);
@@ -1341,7 +1404,7 @@ fn apply_effect(
1404 value_id,
1405 AbstractValue {
1406 kind,
1344 - reason: hashset_of(reason),
1407 + reason: ValueReasonSet::single(reason),
1408 },
1409 );
1410 state.define(into.identifier, value_id);
@@ -1370,7 +1433,7 @@ fn apply_effect(
1433 value_id,
1434 AbstractValue {
1435 kind: from_value.kind,
1373 - reason: from_value.reason.clone(),
1436 + reason: from_value.reason,
1437 },
1438 );
1439 state.define(into.identifier, value_id);
@@ -1487,7 +1550,7 @@ fn apply_effect(
1550 } else {
1551 ValueKind::Frozen
1552 },
1490 - reason: IndexSet::default(),
1553 + reason: ValueReasonSet::default(),
1554 },
1555 );
1556 state.define(into.identifier, value_id);
@@ -1599,7 +1662,7 @@ fn apply_effect(
1662 value_id,
1663 AbstractValue {
1664 kind: from_value.kind,
1602 - reason: from_value.reason.clone(),
1665 + reason: from_value.reason,
1666 },
1667 );
1668 state.define(into.identifier, value_id);
@@ -1615,7 +1678,7 @@ fn apply_effect(
1678 value_id,
1679 AbstractValue {
1680 kind: from_value.kind,
1618 - reason: from_value.reason.clone(),
1681 + reason: from_value.reason,
1682 },
1683 );
1684 state.define(into.identifier, value_id);
@@ -3410,8 +3473,8 @@ fn compute_effects_for_aliasing_signature(
3473 /// since the primary reason is always inserted first, this effectively
3474 /// picks the most specific non-Other reason. We replicate this by
3475 /// preferring any non-Other reason over Other.
3413 -fn primary_reason(reasons: &IndexSet<ValueReason, FxBuildHasher>) -> ValueReason {
3414 - for &r in reasons {
3476 +fn primary_reason(reasons: &ValueReasonSet) -> ValueReason {
3477 + for r in reasons.iter() {
3478 if r != ValueReason::Other {
3479 return r;
3480 }
@@ -3420,32 +3483,32 @@ fn primary_reason(reasons: &IndexSet<ValueReason, FxBuildHasher>) -> ValueReason
3483 }
3484
3485 fn get_write_error_reason(abstract_value: &AbstractValue) -> String {
3423 - if abstract_value.reason.contains(&ValueReason::Global) {
3486 + if abstract_value.reason.contains(ValueReason::Global) {
3487 "Modifying a variable defined outside a component or hook is not allowed. Consider using an effect".to_string()
3425 - } else if abstract_value.reason.contains(&ValueReason::JsxCaptured) {
3488 + } else if abstract_value.reason.contains(ValueReason::JsxCaptured) {
3489 "Modifying a value used previously in JSX is not allowed. Consider moving the modification before the JSX".to_string()
3427 - } else if abstract_value.reason.contains(&ValueReason::Context) {
3490 + } else if abstract_value.reason.contains(ValueReason::Context) {
3491 "Modifying a value returned from 'useContext()' is not allowed.".to_string()
3492 } else if abstract_value
3493 .reason
3431 - .contains(&ValueReason::KnownReturnSignature)
3494 + .contains(ValueReason::KnownReturnSignature)
3495 {
3496 "Modifying a value returned from a function whose return value should not be mutated"
3497 .to_string()
3498 } else if abstract_value
3499 .reason
3437 - .contains(&ValueReason::ReactiveFunctionArgument)
3500 + .contains(ValueReason::ReactiveFunctionArgument)
3501 {
3502 "Modifying component props or hook arguments is not allowed. Consider using a local variable instead".to_string()
3440 - } else if abstract_value.reason.contains(&ValueReason::State) {
3503 + } else if abstract_value.reason.contains(ValueReason::State) {
3504 "Modifying a value returned from 'useState()', which should not be modified directly. Use the setter function to update instead".to_string()
3442 - } else if abstract_value.reason.contains(&ValueReason::ReducerState) {
3505 + } else if abstract_value.reason.contains(ValueReason::ReducerState) {
3506 "Modifying a value returned from 'useReducer()', which should not be modified directly. Use the dispatch function to update instead".to_string()
3444 - } else if abstract_value.reason.contains(&ValueReason::Effect) {
3507 + } else if abstract_value.reason.contains(ValueReason::Effect) {
3508 "Modifying a value used previously in an effect function or as an effect dependency is not allowed. Consider moving the modification before calling useEffect()".to_string()
3446 - } else if abstract_value.reason.contains(&ValueReason::HookCaptured) {
3509 + } else if abstract_value.reason.contains(ValueReason::HookCaptured) {
3510 "Modifying a value previously passed as an argument to a hook is not allowed. Consider moving the modification before calling the hook".to_string()
3448 - } else if abstract_value.reason.contains(&ValueReason::HookReturn) {
3511 + } else if abstract_value.reason.contains(ValueReason::HookReturn) {
3512 "Modifying a value returned from a hook is not allowed. Consider moving the modification into the hook where the value is constructed".to_string()
3513 } else {
3514 "This modifies a variable that React considers immutable".to_string()