Bind ifelse_branching to a CSE temporary while keeping branches lazy#966
Bind ifelse_branching to a CSE temporary while keeping branches lazy#966ChrisRackauckas-Claude wants to merge 2 commits into
ifelse_branching to a CSE temporary while keeping branches lazy#966Conversation
`cse_inside_expr = false` previously conflated "do not descend into the arguments" with "do not bind this node", so an `ifelse_branching` referenced at multiple sites re-emitted its entire `if`/`else` per use. Add a second hook, `cse_bind_expr`, consulted only when `cse_inside_expr` is `false`: when it returns `true`, CSE binds the node as-is (arguments un-CSEd) to a temporary, so all references share one computation. `ifelse_branching` opts in; the default `false` preserves the existing fully-inline behaviour for `Operator`, `getindex` and any other opt-outs. The branches remain lazy (a conditional inside a branch is still emitted within that branch), and one-sided nesting now stays linear, matching `ifelse`. Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Benchmark Results (Julia vlts)Time benchmarks
Memory benchmarks
|
Benchmark Results (Julia v1)Time benchmarks
Memory benchmarks
|
|
CI triage — 26/29 green (all SymbolicUtils own-test matrices,
|
|
The |
Summary
Follow-up to #965 (this was developed on that PR's branch but GitHub dropped the push's synchronize event, so it never attached before the merge — re-landed here on current master).
Multiply-referenced
ifelse_branchingnow shares a singleif/elseinstead of re-emitting it per use site. Previously,cse_inside_expr = falseconflated "do not descend into the arguments" with "do not bind this node": keeping the branches lazy also un-bound the conditional, so e.g.sin(z) + cos(z)withz = ifelse_branching(...)evaluated the taken branch twice.Implementation
A second hook,
Code.cse_bind_expr(sym, f)(defaultfalse), consulted only whencse_inside_exprisfalse. Whentrue,_cse_computebinds the node as-is — arguments un-CSEd — to a CSE temporary;cse!'s id-cache makes every reference in the scope resolve to that one temporary.ifelse_branchingopts in.Operator,getindexand any other opt-outs keep their exact prior fully-inline behaviour (theircse(ex) == exidentity tests are untouched and pass).Generated code for the repro is now:
One semantic boundary (asserted with a comment in the tests): a conditional referenced inside both branches of an enclosing
ifelse_branchingis still emitted per branch — hoisting it above the conditional would evaluate it eagerly, which the laziness contract forbids. One-sided nesting is linear, matchingifelse(previously exponential in depth).Verification
test/conditionals.jl: side-effect counting shows the taken branch fires once (was twice) and the untaken branch never; the CSE structure contains exactly one boundifelse_branching; one-sided nesting emits the same number ofifs asifelse; values matchifelsethroughout.Pkg.test()on this branch (on top of the add some more specialized hashing functions #963 hashing changes): 17771 pass, 3 broken (pre-existing), 0 fail.cse_bind_exprdocumented and added to the codegen manual alongsidecse_inside_expr, plus docs entries for the two operators.Version bumped to 4.36.0. Companion test on the Symbolics side: JuliaSymbolics/Symbolics.jl#1892 (blocked on this releasing).
🤖 Generated with Claude Code