util: make irept destruction depth-safe without recursion#9043
util: make irept destruction depth-safe without recursion#9043tautschnig wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR makes irept destruction depth-safe by bounding recursive deletion in sharing_treet::remove_ref and switching to an explicit work stack when a recursion-depth threshold is reached, preventing stack overflows on deeply nested ireps (e.g., deep SMT-LIB let chains). It also adds a unit test that constructs and destroys an extremely deep irep chain to exercise the new destruction behavior.
Changes:
- Add a thread-local recursion-depth counter in
sharing_treet::remove_refand fall back to iterative subtree deletion beyond a fixed depth bound. - Remove the unused
nonrecursive_destructorhelper and embed the iterative deletion logic directly inremove_ref. - Add a unit test that builds a deeply nested irep chain and verifies destruction completes without a stack overflow.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/util/irep.h |
Bounds recursive destruction and introduces an iterative deep-subtree deletion path inside remove_ref. |
unit/util/irep.cpp |
Adds a unit test that constructs a very deep irep chain and validates destruction doesn’t overflow the stack. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #9043 +/- ##
========================================
Coverage 80.68% 80.69%
========================================
Files 1714 1714
Lines 189501 189556 +55
Branches 73 73
========================================
+ Hits 152902 152959 +57
+ Misses 36599 36597 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Deeply nested ireps (e.g. a deep SMT-LIB let chain) overflow the call stack
when the recursive destructor unwinds them. Bound the destruction recursion:
remove_ref still deletes recursively -- which is as cheap as the natural
recursive destructor for the shallow trees that dominate -- but tracks the
recursion depth in a thread-local counter and, once it reaches a fixed bound
(max_recursion_depth, sized to keep the destructor's own recursion within
~150 KiB of a small ~1 MiB stack), collapses the remainder of the subtree with
an explicit work stack instead. Only the deep tail pays the cost of detaching
children and allocating a work stack; shallow destruction (the overwhelmingly
common case) is unaffected.
This bounded-recursive (hybrid) form is chosen over a simpler always-iterative
destructor, which would pay the detach + work-stack cost on every interior-node
destruction and so regress destruction-heavy workloads. On a SAT-free,
destruction-heavy workload (cbmc --program-only over a tight struct-update
loop; symbolic-execution and simplification churn), median user CPU was:
recursive (baseline) 1.71 s
always-iterative 1.84 s (+~8%)
this hybrid 1.73 s (+~1%)
so the hybrid keeps destruction depth-safe essentially for free.
Add a unit test that builds and destroys chains far deeper than the recursion
bound -- through both positional sub and named_sub children, so both arms of
the iterative deletion path are covered -- and deep enough to overflow a
recursive destructor on the default unit-test stack: it passes with the bounded
destructor and segfaults with a purely recursive one.
Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
a5d5f90 to
89801c5
Compare
Deeply nested ireps (e.g. a deep SMT-LIB let chain) overflow the call stack when the recursive destructor unwinds them. Bound the destruction recursion: remove_ref still deletes recursively -- which is as cheap as the natural recursive destructor for the shallow trees that dominate -- but tracks the recursion depth in a thread-local counter and, once it reaches a fixed bound, collapses the remainder of the subtree with an explicit work stack instead. Only the deep tail pays the cost of detaching children and allocating a work stack; shallow destruction (the overwhelmingly common case) is unaffected, so this avoids the regression on destruction-heavy workloads (symbolic execution and simplification churn) that an always-iterative destructor incurs.
Add a unit test that builds and destroys a chain of ireps far deeper than any plausible call stack; it passes with the bounded destructor and segfaults with a purely recursive one.
Chose a bounded-recursive (hybrid) destructor over an always-iterative one to avoid a regression on destruction-heavy work. SAT-free, destruction-heavy benchmark (
cbmc --program-onlyover a tight struct-update loop — symex/simplification churn), median user CPU: recursive baseline 1.71 s, always-iterative 1.84 s (+~8%), this hybrid 1.73 s (+~1%).