mirror of
https://github.com/aphrody-labs/oxc.git
synced 2026-10-14 22:29:17 +00:00
perf(minifier): reduce allocations when processing if statements (#26666)
- introduce context function `replace_statement_with` for replacing stmts in place - `try_minimize_if` rewrite statements to new statements in place - update `if` stmt instead of recreating it in `handle_if_statement`
This commit is contained in:
1 parent
2ce913e33e
commit
5f729541b4
5 files changed
+107
-71
No files matched your search
@@ -10,32 +10,40 @@ use super::PeepholeOptimizations;
|
||||
|
||||
impl<'a> PeepholeOptimizations {
|
||||
/// `MangleIf`: <https://github.com/evanw/esbuild/blob/v0.24.2/internal/js_parser/js_parser.go#L9860>
|
||||
pub fn try_minimize_if(
|
||||
if_stmt: &mut IfStatement<'a>,
|
||||
ctx: &mut TraverseCtx<'a>,
|
||||
) -> Option<Statement<'a>> {
|
||||
pub fn try_minimize_if(stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
|
||||
let Statement::IfStatement(if_stmt) = stmt else { return };
|
||||
|
||||
// Flip empty consequent so the rest of the function can assume consequent is non-empty.
|
||||
if Self::is_statement_empty(&if_stmt.consequent) {
|
||||
if if_stmt.alternate.is_none() {
|
||||
// `if (a) {}` => `a;`
|
||||
let mut expr = if_stmt.test.take_in(ctx);
|
||||
Self::remove_unused_expression(&mut expr, ctx);
|
||||
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
|
||||
ctx.replace_statement_with(stmt, |stmt, ctx| {
|
||||
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
|
||||
let IfStatement { mut test, span, .. } = if_stmt.unbox();
|
||||
Self::remove_unused_expression(&mut test, ctx);
|
||||
Statement::new_expression_statement(span, test, ctx)
|
||||
});
|
||||
return;
|
||||
}
|
||||
let mut new_consequent = if_stmt.alternate.take().unwrap();
|
||||
let new_consequent = if_stmt.alternate.take().unwrap();
|
||||
|
||||
if let Statement::ExpressionStatement(expr_stmt) = &mut new_consequent {
|
||||
let (op, a) = match &mut if_stmt.test {
|
||||
// `if (!a); else b();` => `a && b();`
|
||||
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
|
||||
(LogicalOperator::And, unary_expr.argument.take_in(ctx))
|
||||
}
|
||||
// `if (a); else b();` => `a || b();`
|
||||
e => (LogicalOperator::Or, e.take_in(ctx)),
|
||||
};
|
||||
let b = expr_stmt.expression.take_in(ctx);
|
||||
let expr = Self::join_with_left_associative_op(if_stmt.span, op, a, b, ctx);
|
||||
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
|
||||
if let Statement::ExpressionStatement(expr_stmt) = new_consequent {
|
||||
ctx.replace_statement_with(stmt, |stmt, ctx| {
|
||||
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
|
||||
let IfStatement { test, span, .. } = if_stmt.unbox();
|
||||
let (op, a) = match test {
|
||||
// `if (!a); else b();` => `a && b();`
|
||||
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
|
||||
(LogicalOperator::And, unary_expr.unbox().argument)
|
||||
}
|
||||
// `if (a); else b();` => `a || b();`
|
||||
e => (LogicalOperator::Or, e),
|
||||
};
|
||||
let b = expr_stmt.unbox().expression;
|
||||
let expr = Self::join_with_left_associative_op(span, op, a, b, ctx);
|
||||
Statement::new_expression_statement(span, expr, ctx)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// `if (!a) {} else x;` => `if (a) x;`
|
||||
@@ -48,41 +56,56 @@ impl<'a> PeepholeOptimizations {
|
||||
|
||||
// Consequent is non-empty from here on.
|
||||
|
||||
if let Some(alternate) = &mut if_stmt.alternate {
|
||||
if let Statement::ExpressionStatement(expr_stmt) = &mut if_stmt.consequent {
|
||||
if let Statement::ExpressionStatement(alternate_expr_stmt) = alternate {
|
||||
if let Some(alternate) = &if_stmt.alternate {
|
||||
if matches!(&if_stmt.consequent, Statement::ExpressionStatement(_)) {
|
||||
if matches!(alternate, Statement::ExpressionStatement(_)) {
|
||||
// `if (a) b(); else c();` => `a ? b() : c();`
|
||||
let test = if_stmt.test.take_in(ctx);
|
||||
let consequent = expr_stmt.expression.take_in(ctx);
|
||||
let alternate = alternate_expr_stmt.expression.take_in(ctx);
|
||||
let expr =
|
||||
Self::minimize_conditional(if_stmt.span, test, consequent, alternate, ctx);
|
||||
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
|
||||
ctx.replace_statement_with(stmt, |stmt, ctx| {
|
||||
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
|
||||
let IfStatement { test, consequent, alternate, span, .. } = if_stmt.unbox();
|
||||
let Statement::ExpressionStatement(a) = consequent else { unreachable!() };
|
||||
let Statement::ExpressionStatement(b) = alternate.unwrap() else {
|
||||
unreachable!()
|
||||
};
|
||||
let a = a.unbox().expression;
|
||||
let b = b.unbox().expression;
|
||||
let expr = Self::minimize_conditional(span, test, a, b, ctx);
|
||||
Statement::new_expression_statement(span, expr, ctx)
|
||||
});
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Normalize: move the `!` out of the test by swapping branches.
|
||||
// Avoid swapping when alternate is an `if` — that risks a worse chain.
|
||||
// `if (!a) return b; else return c;` => `if (a) return c; else return b;`
|
||||
if !matches!(alternate, Statement::IfStatement(_))
|
||||
&& let Expression::UnaryExpression(unary_expr) = &mut if_stmt.test
|
||||
&& let Expression::UnaryExpression(unary_expr) = &if_stmt.test
|
||||
&& unary_expr.operator.is_not()
|
||||
{
|
||||
ctx.replace_expression_with(&mut if_stmt.test, Self::unwrap_unary);
|
||||
std::mem::swap(&mut if_stmt.consequent, alternate);
|
||||
let if_mut = if_stmt.as_mut();
|
||||
let Some(alternate) = &mut if_mut.alternate else { unreachable!() };
|
||||
std::mem::swap(&mut if_mut.consequent, alternate);
|
||||
}
|
||||
}
|
||||
} else if let Statement::ExpressionStatement(expr_stmt) = &mut if_stmt.consequent {
|
||||
let (op, a) = match &mut if_stmt.test {
|
||||
// `if (!a) b();` => `a || b();`
|
||||
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
|
||||
(LogicalOperator::Or, unary_expr.argument.take_in(ctx))
|
||||
}
|
||||
// `if (a) b();` => `a && b();`
|
||||
e => (LogicalOperator::And, e.take_in(ctx)),
|
||||
};
|
||||
let b = expr_stmt.expression.take_in(ctx);
|
||||
let expr = Self::join_with_left_associative_op(if_stmt.span, op, a, b, ctx);
|
||||
return Some(Statement::new_expression_statement(if_stmt.span, expr, ctx));
|
||||
} else if matches!(&if_stmt.consequent, Statement::ExpressionStatement(_)) {
|
||||
ctx.replace_statement_with(stmt, |stmt, ctx| {
|
||||
let Statement::IfStatement(if_stmt) = stmt else { unreachable!() };
|
||||
let IfStatement { test, consequent, span, .. } = if_stmt.unbox();
|
||||
let Statement::ExpressionStatement(b) = consequent else { unreachable!() };
|
||||
let (op, a) = match test {
|
||||
// `if (!a) b();` => `a || b();`
|
||||
Expression::UnaryExpression(unary_expr) if unary_expr.operator.is_not() => {
|
||||
(LogicalOperator::Or, unary_expr.unbox().argument)
|
||||
}
|
||||
// `if (a) b();` => `a && b();`
|
||||
e => (LogicalOperator::And, e),
|
||||
};
|
||||
let b = b.unbox().expression;
|
||||
let expr = Self::join_with_left_associative_op(span, op, a, b, ctx);
|
||||
Statement::new_expression_statement(span, expr, ctx)
|
||||
});
|
||||
return;
|
||||
} else if let Statement::IfStatement(if2_stmt) = &mut if_stmt.consequent
|
||||
&& if2_stmt.alternate.is_none()
|
||||
{
|
||||
@@ -95,7 +118,6 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
|
||||
Self::wrap_to_avoid_ambiguous_else(if_stmt, ctx);
|
||||
None
|
||||
}
|
||||
|
||||
/// Wrap to avoid ambiguous else.
|
||||
|
||||
@@ -621,20 +621,18 @@ impl<'a> PeepholeOptimizations {
|
||||
} else {
|
||||
body[0].span()
|
||||
};
|
||||
let test = if_stmt.unbox().test;
|
||||
let test = Self::minimize_not(test.span(), test, ctx, true);
|
||||
ctx.replace_expression_with(&mut if_stmt.test, |test, ctx| {
|
||||
Self::minimize_not(test.span(), test, ctx, true)
|
||||
});
|
||||
let consequent = if body.len() == 1 {
|
||||
body.remove(0)
|
||||
} else {
|
||||
let scope_id = ctx.create_child_scope_of_current(ScopeFlags::empty());
|
||||
Statement::new_block_statement_with_scope_id(span, body, scope_id, ctx)
|
||||
};
|
||||
let mut if_stmt =
|
||||
IfStatement::new(test.span(), test, consequent, None, ctx);
|
||||
let if_stmt =
|
||||
Self::try_minimize_if(&mut if_stmt, ctx).unwrap_or_else(|| {
|
||||
Statement::IfStatement(ArenaBox::new_in(if_stmt, ctx))
|
||||
});
|
||||
ctx.replace_statement(&mut if_stmt.consequent, consequent);
|
||||
let mut if_stmt = Statement::IfStatement(if_stmt);
|
||||
Self::try_minimize_if(&mut if_stmt, ctx);
|
||||
ctx.notice_change();
|
||||
Self::minimize_statement(if_stmt, stmts, result, ctx);
|
||||
return;
|
||||
|
||||
@@ -436,11 +436,7 @@ impl<'a> Traverse<'a> for PeepholeOptimizations {
|
||||
Statement::IfStatement(s) => {
|
||||
Self::minimize_expression_in_boolean_context(&mut s.test, ctx);
|
||||
Self::try_fold_if(stmt, ctx);
|
||||
if let Statement::IfStatement(if_stmt) = stmt
|
||||
&& let Some(folded_stmt) = Self::try_minimize_if(if_stmt, ctx)
|
||||
{
|
||||
ctx.replace_statement(stmt, folded_stmt);
|
||||
}
|
||||
Self::try_minimize_if(stmt, ctx);
|
||||
}
|
||||
Statement::WhileStatement(s) => {
|
||||
Self::minimize_expression_in_boolean_context(&mut s.test, ctx);
|
||||
|
||||
@@ -433,7 +433,7 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
(options.class && is_class) || (options.function && !is_class)
|
||||
}
|
||||
|
||||
/// Construct a `DroppedSubtreeCollector` borrowing the per-pass change accumulator.
|
||||
/// Construct a [`DroppedSubtreeCollector`] borrowing the per-pass change accumulator.
|
||||
/// Used by the `replace_*` / `drop_*` helpers.
|
||||
#[inline]
|
||||
fn dropped_subtree_collector(&mut self) -> DroppedSubtreeCollector<'a, '_> {
|
||||
@@ -442,7 +442,7 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
|
||||
/// Replace an expression slot. Marks the pass as having mutated the AST.
|
||||
///
|
||||
/// Prefer this over a direct `*slot = new; ctx.notice_change();` pair —
|
||||
/// Prefer this over a direct `*slot = new;` and [`Self::notice_change`] pair —
|
||||
/// the typed helper keeps dropped-subtree bookkeeping, the slot update,
|
||||
/// and the pass revisit request together.
|
||||
#[inline]
|
||||
@@ -469,6 +469,10 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
}
|
||||
|
||||
/// Replace a statement slot. Marks the pass as having mutated the AST.
|
||||
///
|
||||
/// Prefer this over a direct `*slot = new;` and [`Self::notice_change`] pair —
|
||||
/// the typed helper keeps dropped-subtree bookkeeping, the slot update,
|
||||
/// and the pass revisit request together.
|
||||
#[inline]
|
||||
pub fn replace_statement(&mut self, slot: &mut Statement<'a>, new: Statement<'a>) {
|
||||
self.dropped_subtree_collector().visit_statement(slot);
|
||||
@@ -476,6 +480,22 @@ impl<'a> TraverseCtx<'a, MinifierState<'a>> {
|
||||
self.state.record_ast_change();
|
||||
}
|
||||
|
||||
/// Replace a statement slot with a value built from the owned old statement.
|
||||
///
|
||||
/// Prefer this over `take_in` followed by [`Self::replace_statement`]. It avoids
|
||||
/// leaving a dummy node in the arena. The closure must report any discarded
|
||||
/// subtrees through the `drop_*` helpers; values moved into its result are not dropped.
|
||||
#[inline]
|
||||
pub fn replace_statement_with(
|
||||
&mut self,
|
||||
slot: &mut Statement<'a>,
|
||||
replacer: impl FnOnce(Statement<'a>, &mut Self) -> Statement<'a>,
|
||||
) {
|
||||
let ctx = &mut *self;
|
||||
slot.replace_with(|old| replacer(old, ctx));
|
||||
self.state.record_ast_change();
|
||||
}
|
||||
|
||||
/// Replace an assignment-target-property slot. Marks the pass as having mutated the AST.
|
||||
#[inline]
|
||||
pub fn replace_assignment_target_property(
|
||||
|
||||
@@ -5,9 +5,9 @@ checker.ts:
|
||||
sys deallocs: 122
|
||||
sys alloc bytes: 14996632 # 15.00 MB
|
||||
sys peak growth: 10169560 # 10.17 MB
|
||||
arena allocs: 59899
|
||||
arena allocs: 56226
|
||||
arena reallocs: 21411
|
||||
arena size: 16569784 # 16.57 MB
|
||||
arena size: 16507656 # 16.51 MB
|
||||
|
||||
App.tsx:
|
||||
file size: 415342 # 415.34 kB
|
||||
@@ -16,9 +16,9 @@ App.tsx:
|
||||
sys deallocs: 61
|
||||
sys alloc bytes: 2128563 # 2.13 MB
|
||||
sys peak growth: 1612427 # 1.61 MB
|
||||
arena allocs: 8350
|
||||
arena allocs: 7489
|
||||
arena reallocs: 2709
|
||||
arena size: 2719488 # 2.72 MB
|
||||
arena size: 2704912 # 2.70 MB
|
||||
|
||||
RadixUIAdoptionSection.jsx:
|
||||
file size: 2520 # 2.52 kB
|
||||
@@ -38,9 +38,9 @@ pdf.mjs:
|
||||
sys deallocs: 2427
|
||||
sys alloc bytes: 5316219 # 5.32 MB
|
||||
sys peak growth: 3693363 # 3.69 MB
|
||||
arena allocs: 20924
|
||||
arena allocs: 19526
|
||||
arena reallocs: 7696
|
||||
arena size: 5782208 # 5.78 MB
|
||||
arena size: 5757664 # 5.76 MB
|
||||
|
||||
antd.js:
|
||||
file size: 6686316 # 6.69 MB
|
||||
@@ -49,9 +49,9 @@ antd.js:
|
||||
sys deallocs: 860
|
||||
sys alloc bytes: 29976375 # 29.98 MB
|
||||
sys peak growth: 19934749 # 19.93 MB
|
||||
arena allocs: 187025
|
||||
arena allocs: 181720
|
||||
arena reallocs: 63323
|
||||
arena size: 43370040 # 43.37 MB
|
||||
arena size: 43279864 # 43.28 MB
|
||||
|
||||
binder.ts:
|
||||
file size: 193077 # 193.08 kB
|
||||
@@ -60,9 +60,9 @@ binder.ts:
|
||||
sys deallocs: 33
|
||||
sys alloc bytes: 963032 # 963.03 kB
|
||||
sys peak growth: 657404 # 657.40 kB
|
||||
arena allocs: 3311
|
||||
arena allocs: 2923
|
||||
arena reallocs: 834
|
||||
arena size: 1083312 # 1.08 MB
|
||||
arena size: 1077056 # 1.08 MB
|
||||
|
||||
kitchen-sink.tsx:
|
||||
file size: 732898 # 732.90 kB
|
||||
@@ -71,6 +71,6 @@ kitchen-sink.tsx:
|
||||
sys deallocs: 1854
|
||||
sys alloc bytes: 5304692 # 5.30 MB
|
||||
sys peak growth: 3707457 # 3.71 MB
|
||||
arena allocs: 31927
|
||||
arena allocs: 31143
|
||||
arena reallocs: 12185
|
||||
arena size: 8688104 # 8.69 MB
|
||||
arena size: 8673608 # 8.67 MB
|
||||
Reference in new issue
Block a user