Repository navigation
Add node-based catamorphism POC - #31
Adam-Vandervorst wants to merge 60 commits into
Conversation
…zer more to work with. ~5% improvement to saturated val_count benchmark
…ad memory when retrieving the refcount of the empty node
|
@luketpeterson any problem merging this? |
In concept no. There is a (small) bit of work to make the API is consistent with the cata that's there already. |
|
Giving Fable some time with it: BlockingB1 —
|
|
Path byte should not be represented in prefix. |
…ld cata, and picking up another 5% perf
… wasn't offering a meaningful speedup when switched off
…t misunderstand the contract Harmonizing description of the jumping cata, so we don't have `prefix` and `sub_path` as two ways to refer to the same thing
Deleting two crufty tests that are already expressed in the macro
…ther than always starting from the root Updating CatamorphismDebug trait to use iterative cata traversal Deleting old implementation of caching cata body, since it no longer has any users
…at took too long to run under miri
…ed trait Implementing ZipperConcrete on WriteZipper flavors Dropping a handful of unneeded bounds on catamorphism traits Ripping out parallel val_count (and goat_val_count) implementations and benchmarks
…removing usused `A: Allocator` parameter Fixing arena_compact benchmarks
|
Wow that took a lot more work than expected to get the API into shape. But it should be ready to merge now. |
|
Fable 5.1 1. Recursive engine misplaces the value in LineListNode Case 9 with a ≥3-byte child keyBlocker. map = {a, abcd, abce} // plain inserts
recursive rebuild -> {ab, abcd, abce} // value "a" lands at "ab"
Fix: mirror the Case 7 fix — summarize the child under 2. Iterative engine aborts (debug) / commits UB (release) on dangling branchesBlocker for relying on the iterative engine. map = {aa, ab, ba, bb, b}
map.remove_val_at(b"ba", false); // prune = false
map.remove_val_at(b"bb", false);
CatamorphismCached::val_count(&map) // 3 — recursive engine guards the empty node
CatamorphismCachedIterative::val_count(&map) // SIGABRT: non-unwinding UB-check panic in refcount()A no-prune removal leaves a valueless branch that is logical structure ( Fix: guard with 3.
|
|
One more Iterative engine hangs on a LineListNode shape left behind by
Standalone test (drop into use pathmap::PathMap;
use pathmap::zipper::*;
use pathmap::morphisms::{CatamorphismCached, CatamorphismCachedIterative};
#[test]
fn drop_head_shared_first_byte_shape() {
let mut m = PathMap::<()>::new();
m.set_val_at(b"aaa", ());
m.set_val_at(b"bab", ());
assert!(m.write_zipper().join_k_path_into(1, false));
assert_eq!(m.iter().map(|(k, _)| k).collect::<Vec<_>>(), vec![b"aa".to_vec(), b"ab".to_vec()]);
let mut rz = m.read_zipper();
rz.descend_to_byte(b'a');
assert_eq!(rz.child_count(), 2);
assert!(rz.descend_indexed_byte(1).is_some(), "indexed descent cannot reach the second child"); // fails here today
assert_eq!(CatamorphismCached::val_count(&m), 2); // debug build: Case 7 assert
assert_eq!(CatamorphismCachedIterative::val_count(&m), 2); // never returns
}Fix that makes this pass (and keeps both full suites green): in let legal_overlap = overlap == 1 && (
(!self.is_child_ptr::<0>() && key0.len() == 1) ||
(!self.is_child_ptr::<1>() && key0.len()==1 && key1.len()==1 ));— so the shape gets factored into the canonical |
|
ACT should get a fast recursive implementation of cata, modeled on the ACT val_count in master, and val_count benchmarks that can easily be back-ported to master. |
Implement subtraction-aware recursive cached morphisms for SubtractZipper, delegate unaffected subtrees, and keep value counting in the morphism API. Adapt incoming tests to the new API.
# Conflicts: # src/write_zipper.rs
…cover it. (~3-5x uplift)
Bench A/B vs base: donejob log · 2026-11-02 19:07:13 UTC base 9598a1a → head 9135c2f, 3 round(s), median of each run averaged; negative is faster
42 case(s) moved more than 5%
Full tables per bench are in the job log and the bench-out artifact. |
… the stem of a prefix zipper too)
|
I think this PR is finally ready to go. However, the one last piece of validation I want to do is see if the bespoke val_counts that are getting replaces got any slower by moving the implementation over to the recursive catamorphism. |
|
Here are the complete benchmark results. Bottom line, recursive cata is massively faster than iterative cata, as discussed above. Since val_count is now subsumed by recursive cata, the val_count benchmark results are a bit more nuanced. There is an essential tradeoff in the implementation whether to count the value first or to visit the downstream first. Recursive cata made a different tradeoff than the hard-coded val_count resulting in better performance for catamorphism workloads, and about a ~10-20% uplift on "real" tries (binary trie shape), roughly neutral on the superdense benchmark, but a similar penalty on the "sparse" tries (a bunch of non-branching paths). On ACT, there was no recursive cata at all. Implementing one gave a 3-8x speedup over the iterative cata but a 5-40x perf improvement in val_count vs the implementation on the ACTZipper! |
I purpose we use this paradigm under the cata interface.