Preserve structural sharing in update, intersect and leaf combine - #35
Open
arthaud wants to merge 1 commit into
Open
Preserve structural sharing in update, intersect and leaf combine#35arthaud wants to merge 1 commit into
arthaud wants to merge 1 commit into
Conversation
A Patricia tree is immutable and shared through `Rc`, so an operation
whose result equals its input should return the input itself rather than
rebuild it. `merge_trees` already did this; three other places did not,
and because they allocated fresh nodes unconditionally they also defeated
the `Rc::ptr_eq` fast paths that `merge_trees`, `is_tree_subset_of` and
`is_tree_leq` rely on for sublinear behaviour on later operations. That
matters here: fixpoint iteration merges near-identical environments over
and over, and every lost identity makes the next round more expensive.
This mirrors what the C++ implementation does in PatriciaTreeCore.h.
- `update_node_by_key` now returns the existing branch when the rebuilt
child is pointer-equal to the old one, rather than rebuilding the whole
root-to-leaf path. The comment asking for exactly this has been there
since the file was written; C++ does it in `update_leaf_by_key`.
- `intersect_trees` now returns `s` when both subtrees come back
unchanged, matching `intersect_trees` in C++.
- The leaf combiner keeps the existing leaf when the combined value
equals the value already stored, instead of always allocating a new
one. This is the C++ `update_leaf_internal` behaviour, and it is what
makes the two checks above fire at all: for a set every value is `()`,
so previously *every* shared leaf was reallocated during a union or
intersection and no subtree ever looked unchanged.
Comparing a value requires `V: Eq`, which is now a bound on
`get_leaf_combine_with_value_op_semantics`, `union_with` and
`intersect_with`. All three are `pub(crate)` and every caller already
satisfies it, so no public signature changes. C++ has the same
requirement, via `Value::equals` in its `Value` contract.
Measured with a counting global allocator on a 10,000 element
`PatriciaTreeSet<u32>`, for operations whose result equals their input:
remove(absent key) 14 -> 0 allocations
intersect_with(superset) 19999 -> 0 allocations
union_with(subset) 927 -> 0 allocations
`insert` of an already present key still costs one rebuilt path; it
allocates its leaf before consulting the tree, and fixing it means adding
`V: Eq` to `PatriciaTreeMap::upsert`, which is a public signature. Left
for a separate change.
Test plan: `cargo test` — 55 tests pass, including two new ones covering
that no-op operations keep the root pointer and that a combine which does
change a value still updates it. `cargo clippy --all-targets` emits the
same set of warnings as main; `cargo fmt --check` reports no diff for
this file.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KRRfrza7QDBPhTT3yAS8NV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Patricia tree is immutable and shared through
Rc, so an operation whose result equals its input should return the input itself rather than rebuild it.merge_treesalready did this; three other places did not, and because they allocated fresh nodes unconditionally they also defeated theRc::ptr_eqfast paths thatmerge_trees,is_tree_subset_ofandis_tree_leqrely on for sublinear behaviour on later operations. That matters here: fixpoint iteration merges near-identical environments over and over, and every lost identity makes the next round more expensive.This mirrors what the C++ implementation does in PatriciaTreeCore.h.
update_node_by_keynow returns the existing branch when the rebuilt child is pointer-equal to the old one, rather than rebuilding the whole root-to-leaf path. The comment asking for exactly this has been there since the file was written; C++ does it inupdate_leaf_by_key.intersect_treesnow returnsswhen both subtrees come back unchanged, matchingintersect_treesin C++.The leaf combiner keeps the existing leaf when the combined value equals the value already stored, instead of always allocating a new one. This is the C++
update_leaf_internalbehaviour, and it is what makes the two checks above fire at all: for a set every value is(), so previously every shared leaf was reallocated during a union or intersection and no subtree ever looked unchanged.Comparing a value requires
V: Eq, which is now a bound onget_leaf_combine_with_value_op_semantics,union_withandintersect_with. All three arepub(crate)and every caller already satisfies it, so no public signature changes. C++ has the same requirement, viaValue::equalsin itsValuecontract.Measured with a counting global allocator on a 10,000 element
PatriciaTreeSet<u32>, for operations whose result equals their input:insertof an already present key still costs one rebuilt path; it allocates its leaf before consulting the tree, and fixing it means addingV: EqtoPatriciaTreeMap::upsert, which is a public signature. Left for a separate change.Test plan:
cargo test— 55 tests pass, including two new ones covering that no-op operations keep the root pointer and that a combine which does change a value still updates it.cargo clippy --all-targetsemits the same set of warnings as main;cargo fmt --checkreports no diff for this file.