perf(es/minifier): Apply pending substitutions in batch for if-dense statement lists - #12079
Conversation
…statement lists The optimizer flushed pending substitutions into each `if` statement individually before `merge_similar_ifs` (one Finalizer + one NormalMultiReplacer walk per `if`). On typescript.js this produced ~68k double walks per optimize() call with 0.4-1.7% hit rates. Apply the substitutions to the whole statement list in a single batch walk when the list is if-dense (>= 16 ifs), and keep the per-if loop otherwise. Both forms are semantically equivalent: the substitution maps only mutate via consumption of applied entries, which happens in the same statement order either way. es/minifier/libs benchmarks: 2.5-13.8% faster across all 12 lib fixtures (typescript -13.8%, echarts -11.8%, lodash -10.5%), byte-identical output on the entire fixture corpus.
🦋 Changeset detectedLatest commit: 85c8c60 The changes in this PR will be included in the next version bump. Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will not alter performance
Comparing Footnotes
|
|
Donny/강동윤 (@kdy1) I am testing a few optimizations with codespeed that actually move the needle when LTO is enabled. should there be someway to have LTO enabled for the benchmark? like adding a label to check LTO baseline? |
Description:
Profiling the minifier on large inputs showed that
inline_with_multi_replaceris invoked tens of thousands of times peroptimize()call (each call = oneFinalizerwalk + oneNormalMultiReplacerwalk over a small subtree), with hit rates of only 0.4–1.7% (replacer) and 0.006–0.04% (finalizer) — i.e. ~99% of the walks change nothing. About half of them came frommerge_similar_ifs, which flushed pending substitutions into eachifstatement individually before merging (34k double walks per run on typescript.js alone).This PR applies pending substitutions to the whole statement list in a single batch walk when the list is
if-dense (>= 16ifs), and keeps the per-ifloop for sparse lists. The two forms are semantically equivalent: the substitution maps do not change during application except for consumption of applied entries, which happens in the same statement order either way. The density gate preserves the win where batching pays off (Σ(if subtrees) ≈ Σ(list)) without regressingif-sparse inputs, where per-ifwalks are already the cheaper strategy.Output: byte-identical on the entire fixture corpus — the full suite passes with zero fixture updates required (including the big projects/bench lib fixtures).
Test plan:
cargo fmt --allcargo clippy --all --all-targets -- -D warningscargo test -p swc_ecma_minifier(fully green, no fixture updates)./scripts/exec.sh(2128/2128 pass)BREAKING CHANGE:
None.
Related issue (if exists):
#11517 (the pre-existing workaround this change preserves, but cheaper)