Skip to content

Fix exponential blow-up in skip_stages on chains of let-bound selects#9147

Open
abadams wants to merge 3 commits into
mainfrom
abadams/fix_exponential_skip_stages
Open

Fix exponential blow-up in skip_stages on chains of let-bound selects#9147
abadams wants to merge 3 commits into
mainfrom
abadams/fix_exponential_skip_stages

Conversation

@abadams
Copy link
Copy Markdown
Member

@abadams abadams commented May 17, 2026

In SkipStages::visit(Select), the two branches' per-Func .used / .loaded predicates were combined as (t_used && cond) || (f_used && !cond). When both branches contributed the same Expr -- which is exactly what happens when both branches read the same let-stashed FuncInfo from an outer let -- make_or could not recognise the And nodes as equivalent (they aren't same_as even when their operands are), so the predicate roughly doubled in size at every nested Select. A long chain of CSE'd lets where each let value contains a Select then drove the predicate size to 2^N, well past the point where allocating the IR is feasible.

Combine the two branches with select(cond, t, f) instead, and add a make_select helper that collapses select(c, X, X) -> X and the constant-cond cases. When both branches contributed the same Expr, make_select drops the condition immediately and the chain stays linear.

The new correctness test (many_inlined_selects.cpp) constructs a 500- element CSE'd let chain whose values each carry a Param-gated Select, then feeds the chain into a final Select. With the bug present this test would not terminate -- skip_stages would crash allocating ~2^500 IR nodes long before any reasonable timeout fired.

abadams and others added 3 commits May 17, 2026 14:15
In SkipStages::visit(Select), the two branches' per-Func .used / .loaded
predicates were combined as `(t_used && cond) || (f_used && !cond)`. When
both branches contributed the same Expr -- which is exactly what
happens when both branches read the same let-stashed FuncInfo from an
outer let -- make_or could not recognise the And nodes as equivalent
(they aren't same_as even when their operands are), so the predicate
roughly doubled in size at every nested Select. A long chain of CSE'd
lets where each let value contains a Select then drove the predicate
size to 2^N, well past the point where allocating the IR is feasible.

Combine the two branches with `select(cond, t, f)` instead, and add a
make_select helper that collapses `select(c, X, X) -> X` and the
constant-cond cases. When both branches contributed the same Expr,
make_select drops the condition immediately and the chain stays linear.

The new correctness test (many_inlined_selects.cpp) constructs a 500-
element CSE'd let chain whose values each carry a Param<bool>-gated
Select, then feeds the chain into a final Select. With the bug present
this test would not terminate -- skip_stages would crash allocating
~2^500 IR nodes long before any reasonable timeout fired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When an id is only touched on one branch of the Select, the previous
code passed an undefined Expr to a `combine` helper that then turned
`undefined` into const_false and built a `select(cond, X, false)` --
which is just `X && cond` dressed up as a select. Call make_and directly
in those cases and keep make_select for the both-branches case, where
the `select(c, X, X) -> X` collapse is the whole point. Also factor the
"merge into old" body into a small helper to remove the duplication.

No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lowercase the Func name, drop the unnecessary top-level select and
output schedule, and make each chain entry depend on chain.back() so
nothing gets eliminated as dead. The test still reproduces the pre-fix
exponential blow-up (verified by reverting the fix: it times out at
30s on a 500-element chain).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 88.23529% with 4 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@d58798a). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/SkipStages.cpp 88.23% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #9147   +/-   ##
=======================================
  Coverage        ?   69.77%           
=======================================
  Files           ?      255           
  Lines           ?    77556           
  Branches        ?    18541           
=======================================
  Hits            ?    54118           
  Misses          ?    17962           
  Partials        ?     5476           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants