mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
perf(minifier): index SymbolValues by SymbolId (#22441)
Stacked on top of #22425. ## Summary Swap `SymbolValues`'s backing store from `FxHashMap<SymbolId, SymbolValue>` to `IndexVec<SymbolId, Option<SymbolValue>>`. Symbol IDs are dense `u32`s, so indexed access drops the hash + probe on every hot path — `inline_identifier_reference`, `is_symbol_mutated`, and the dead-code checks in `remove_unused_expression` / `minimize_statements`. The buffer is sized once from `Scoping::symbols_len()` at `MinifierState::new` and reset in place between peephole iterations, so the whole fixed-point loop stays on the indexed-write fast path. No minifier pass currently mints `SymbolId`s mid-run (verified across `peephole/`, `normalize/`, the mangler, and `keep_var.rs` — only `traverse_context/` defines `generate_uid_name` / `create_symbol`, with zero callers), so `init_value` is a single indexed write. If a future pass starts minting UIDs, that write panics on out-of-range — the signal to add a grow path. Local harness running `Compressor::build_with_scoping` against `TestFiles::minimal()` (`RadixUIAdoptionSection.jsx`, `react.development.js`, `cal.com.tsx`, `binder.ts`), 27 paired cycles in randomized order after warm-up: | binary | min | median | p75 | std | |-----------------------|----------:|----------:|----------:|---------:| | bitset base (#22425) | 6 905 125 | 6 958 167 | 6 975 501 | 37 944 | | this PR on top | 6 693 875 | 6 750 875 | 6 774 626 | 40 482 | Paired delta: this PR wins **27 / 27** cycles, median **−207 µs (−2.97 %)**, sign-test p < 0.000001. `cargo test -p oxc_minifier --tests` passes 494/494; allocation snapshot shows reduced sys allocs on every file.
This commit is contained in:
1 parent
d782b78d5f
commit
217d7d8a1c
5 files changed
+44
-18
No files matched your search
@@ -30,7 +30,8 @@ impl<'a> Compressor<'a> {
|
||||
options: CompressOptions,
|
||||
) -> u8 {
|
||||
let max_iterations = options.max_iterations;
|
||||
let state = MinifierState::new(program.source_type, options, /* dce */ false);
|
||||
let state =
|
||||
MinifierState::new(program.source_type, options, /* dce */ false, &scoping);
|
||||
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
|
||||
let normalize_options = NormalizeOptions {
|
||||
convert_while_to_fors: true,
|
||||
@@ -54,7 +55,7 @@ impl<'a> Compressor<'a> {
|
||||
options: CompressOptions,
|
||||
) -> u8 {
|
||||
let max_iterations = options.max_iterations;
|
||||
let state = MinifierState::new(program.source_type, options, /* dce */ true);
|
||||
let state = MinifierState::new(program.source_type, options, /* dce */ true, &scoping);
|
||||
let mut ctx = ReusableTraverseCtx::new(state, scoping, self.allocator);
|
||||
let normalize_options = NormalizeOptions {
|
||||
convert_while_to_fors: false,
|
||||
|
||||
@@ -160,7 +160,7 @@ impl<'a> PeepholeOptimizations {
|
||||
|
||||
impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||
fn enter_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
ctx.state.symbol_values.clear();
|
||||
ctx.state.symbol_values.reset();
|
||||
ctx.state.proto_write_symbols.clear();
|
||||
ctx.state.changed = false;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use oxc_ecmascript::constant_evaluation::ConstantValue;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use oxc_data_structures::stack::NonEmptyStack;
|
||||
use oxc_semantic::Scoping;
|
||||
use oxc_span::SourceType;
|
||||
use oxc_str::Str;
|
||||
use oxc_syntax::symbol::SymbolId;
|
||||
@@ -33,13 +34,18 @@ pub struct MinifierState<'a> {
|
||||
}
|
||||
|
||||
impl MinifierState<'_> {
|
||||
pub fn new(source_type: SourceType, options: CompressOptions, dce: bool) -> Self {
|
||||
pub fn new(
|
||||
source_type: SourceType,
|
||||
options: CompressOptions,
|
||||
dce: bool,
|
||||
scoping: &Scoping,
|
||||
) -> Self {
|
||||
Self {
|
||||
source_type,
|
||||
options,
|
||||
dce,
|
||||
pure_functions: FxHashMap::default(),
|
||||
symbol_values: SymbolValues::default(),
|
||||
symbol_values: SymbolValues::new(scoping.symbols_len()),
|
||||
class_symbols_stack: ClassSymbolsStack::new(),
|
||||
proto_write_symbols: FxHashSet::default(),
|
||||
changed: false,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use oxc_ecmascript::constant_evaluation::ConstantValue;
|
||||
use oxc_index::IndexVec;
|
||||
use oxc_syntax::{scope::ScopeId, symbol::SymbolId};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -28,21 +27,41 @@ pub struct SymbolValue<'a> {
|
||||
pub scope_id: ScopeId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// Per-symbol scratch store indexed by `SymbolId`.
|
||||
///
|
||||
/// Symbol IDs are dense `u32`s, so an indexed `IndexVec` lookup beats a
|
||||
/// `FxHashMap` (hash + probe) on every hot path in the peephole pass.
|
||||
///
|
||||
/// Sized once from `Scoping::symbols_len()`; no minifier pass mints new
|
||||
/// symbols, so `init_value` panics on out-of-range — that's the signal to
|
||||
/// add a grow path.
|
||||
#[derive(Debug)]
|
||||
pub struct SymbolValues<'a> {
|
||||
values: FxHashMap<SymbolId, SymbolValue<'a>>,
|
||||
values: IndexVec<SymbolId, Option<SymbolValue<'a>>>,
|
||||
}
|
||||
|
||||
impl<'a> SymbolValues<'a> {
|
||||
pub fn clear(&mut self) {
|
||||
self.values.clear();
|
||||
pub(crate) fn new(len: usize) -> Self {
|
||||
let mut values = IndexVec::with_capacity(len);
|
||||
values.resize_with(len, || None);
|
||||
Self { values }
|
||||
}
|
||||
|
||||
/// Reset slots to `None` without releasing the buffer, so the next peephole
|
||||
/// iteration's `init_value` stays on the indexed-write fast path.
|
||||
pub fn reset(&mut self) {
|
||||
for slot in &mut self.values {
|
||||
*slot = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn init_value(&mut self, symbol_id: SymbolId, symbol_value: SymbolValue<'a>) {
|
||||
self.values.insert(symbol_id, symbol_value);
|
||||
self.values[symbol_id] = Some(symbol_value);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_symbol_value(&self, symbol_id: SymbolId) -> Option<&SymbolValue<'a>> {
|
||||
self.values.get(&symbol_id)
|
||||
self.values.get(symbol_id)?.as_ref()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
File | File size || Sys allocs | Sys reallocs || Arena allocs | Arena reallocs
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
checker.ts | 2.92 MB || 238 | 37 || 152662 | 28244
|
||||
checker.ts | 2.92 MB || 226 | 37 || 152662 | 28244
|
||||
|
||||
cal.com.tsx | 1.06 MB || 16296 | 46 || 37156 | 4570
|
||||
cal.com.tsx | 1.06 MB || 16287 | 46 || 37156 | 4570
|
||||
|
||||
RadixUIAdoptionSection.jsx | 2.52 kB || 24 | 6 || 31 | 6
|
||||
|
||||
pdf.mjs | 567.30 kB || 4026 | 417 || 47474 | 7746
|
||||
pdf.mjs | 567.30 kB || 4016 | 417 || 47474 | 7746
|
||||
|
||||
antd.js | 6.69 MB || 7638 | 1657 || 333609 | 69349
|
||||
antd.js | 6.69 MB || 7625 | 1657 || 333609 | 69349
|
||||
|
||||
binder.ts | 193.08 kB || 54 | 23 || 7076 | 824
|
||||
binder.ts | 193.08 kB || 47 | 23 || 7076 | 824
|
||||
|
||||
Reference in new issue
Block a user