Repository navigation
Conversation
… panics
- Node.clone copied keys and children but not values, so every value in a
node was zeroed after the first copy-on-write following Clone. Only sets
(V = struct{}) had ever been tested.
- orderstat Iterator.Rank ascended exactly one frame, so it under-counted
for any tree of height 3 or more. It now sums every ancestor frame via a
new LowLevelIterator.Frame accessor.
- Next past the end and Prev before the beginning descended into
children[count+1] / children[-1]. Both now stay invalid; Next from before
the beginning yields the first key and Prev from past the end yields the
last.
- Add Map.Verify, a structural and augmentation invariant checker for
tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- go.mod: go 1.26.0, no dependencies. testify's few uses replaced with
plain testing helpers. go fix applied (interface{} -> any, range over int).
- Rename ExampleBlog, which vet rejects and which broke the orderstat test
build since Go 1.24.
- New tests: differential test of Map against a model with interleaved
Clone/Reset across up to eight trees, iterator over-run edge cases at
heights 1-3, clone isolation for a Map with string values, a fuzz target,
and orderstat Rank/SeekNth checks at every height including 300k items.
All run Verify.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… lists
Replace internal/abstract with the public package aug:
- Nodes are slice-backed (one []entry{v, k} plus []*Node for interior
nodes) so the degree is a per-tree runtime setting. Clone copies and
free clears only the live prefix of each node. DefaultDegree is 16.
- FreeList is an interface; NewFreeList gives a bounded mutex-guarded
list (the default, DefaultFreeListSize 32) and SyncPoolFreeList a
sync.Pool-backed one shared per instantiation. Free lists may be shared
by trees of different degrees.
- New(cmp, updater, ...Option) returns *Map; WithDegree and WithFreeList
are the options. Reset is now Clear. Get no longer builds an iterator.
- Merging a shared sibling no longer clones it just to free it.
- Node exposes GetValue.
btree, orderstat and interval now wrap *aug.Map and expose New/NewSet
returning pointers, plus Contains on sets. orderstat's SeekNth leaves the
iterator invalid for an out-of-range rank instead of panicking.
Tests run the differential, edge, rank and fuzz suites across degrees, and
add free-list recycling, sharing across degrees, option validation and
concurrent snapshot reader tests.
On the clone -> 200 rekeys -> Clear round with a 24-byte key and pointer
value, degree 16 with a 1024-node free list runs at 50µs per round for a
20k-host fleet versus 53µs for google/btree at degree 16, with 2 allocs
per round.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…seek - aug.Monoid (Of, Combine) and aug.Group (adds Uncombine) describe an augmentation as per-entry contributions; aug.MonoidUpdater turns one into an Updater, incremental for Groups and recomputing otherwise. Optional aug.Equaler stops propagation when nothing changed and aug.Folder folds node spans in one call. Ready-made: Count, Sum(f), PairOf(a, b). - UpdateInfo carries RelevantValue and a new Replacement action with the previous entry, so value-derived aggregates stay correct when an existing key is upserted. Updaters receive UpdateInfo[K, V, A]. - Map.Total, Map.Prefix(key) and Map.Aggregate(lo, hi); Iterator.Prefix() and Iterator.SeekWhere(pred), a single descent to the first entry where a monotone predicate on the running prefix flips. - orderstat is rewritten over aug.Count: Rank(key), Nth, Count(lo, hi) on Map and Set; Iterator.Rank() returns the insertion rank when invalid. - Tests: value-derived Sum, Max (a non-group) and Pair aggregates checked against a model across degrees with clones and replacements, plus benchmarks for the rank operations. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
aug.Cursor embeds Iterator. Its first write pins the root-to-leaf path (copy-on-write per frame); SetValue, Delete, Rekey and a hinted Upsert then act in place when the leaf can absorb the change, maintaining augmentations up the pinned path, and otherwise fall back to the top-down algorithm plus a re-seek. Delete leaves the cursor on the successor, Rekey on the moved entry (a nearby move after the in-place delete re-inserts through the hinted Upsert without another descent), Upsert on the upserted entry. Exposed as btree.MapCursor/SetCursor and orderstat.Cursor with Rank and SeekNth. A differential test drives every cursor operation against a model at degrees 2, 3 and 16 with clones taken mid-stream and Verify after each checkpoint. On a 20k-entry map with 24-byte keys and a count augmentation, moving an entry to a nearby key through the cursor takes 266 ns versus 424 ns for seek, Delete and Upsert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Map.All, Backward, Range(lo, hi) and From(lo) return iter.Seq2 views built on a recursive node walk; btree.Set and orderstat.Set expose them as iter.Seq. Map.Min and Max walk to the ends. - Measured at 1M entries: the views cost the same per entry as the stateful Iterator once the loop body touches the data; with an empty body the range-over-func rewrite adds about 2.7 ns per yield. An All built on the Iterator was slower than the recursive walk and was dropped. - FuzzCursor drives every cursor operation from a byte script against a model with snapshots; TestCursorWriterWithSnapshotReaders runs a cursor writer against readers of the snapshots it hands out under the race detector. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- orderstat.SeekNth descends by child counts through the low-level iterator again: 94 ns versus 310 ns for the SeekWhere form at 1M entries. SeekWhere remains the general primitive. - DefaultFreeListSize is 256: a clone that takes a few hundred writes and is cleared frees about one node per touched path, and 32 spilled most of them to the GC (70 µs versus 50 µs per round at 20k entries). - Measured that atomic refcounts cost 2-7% against plain arithmetic; the refcount overhead is the memory traffic, so atomics stay. - With Cursor.Rekey the scheduler-shaped round runs at 42 µs against 53 µs for google/btree at degree 16. README gets a performance section. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mples - Node accessors lose their Get prefix: Aug, Key, Value, ChildAug; LowLevelIterator.Child is ChildAug; Iterator.Cur is Key, next to Value. - interval.New and NewSet take a Bounds struct (Compare, Key, End, optional HasEnd and CompareIntervals with documented defaults) instead of five positional functions; the exported Cmp type is gone. Map and Set gain Overlapping(bounds) as a range-over-func view. - Examples for orderstat (Rank, Count, Nth, SeekNth) and for a cursor sweep that charges the least loaded host in place; the blog post carries a note about the API having moved on. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
staticcheck reported an unused helper, an unused example method and two bytes.Compare == 0 sites; fixed, and staticcheck joins vet in CI. The interval test suite now uses math/rand/v2 with a random seed logged per test or benchmark instead of time-seeded math/rand. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
aug.NewOrdered, btree.NewOrdered/NewOrderedSet and orderstat.NewOrdered/ NewOrderedSet install a node search that compares with < directly instead of calling the comparator per probe. The comparator path is unchanged. On int keys this is 15-25% faster on every point operation; at degree 32 Get at 1M entries takes 141 ns against 187 ns for google/btree at the same degree. The differential test runs both constructors at every degree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
TestOverlapProperty mixes random points and ranges, deletes, clones and Verify at degrees 2, 3 and 16 and compares Overlapping against a reference overlap definition after every thousand operations. BoundsOf builds Bounds for interval types that expose Key and End methods. Stale "abstract" wording in the overlap scan commentary is fixed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pied Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…veat - SeekWhere with a predicate that breaks monotonicity must still leave the iterator valid at some entry or past the end, with Prefix() matching what it returned; always-false and always-true predicates are checked too. - A 2M-entry degree-2 tree (height above the iterator's ten inline frames) is driven through seeks, ranks, views and cursor writes on a clone so the spilled frame stack is exercised on every path. Skipped under -short. - NewOrdered documents that NaN keys are unsupported. - 60 s fuzz runs of FuzzMap and FuzzCursor found nothing. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… scans SeekGE and SeekLT return whether the sought key exists, saving callers a compare; SeekLE and SeekGT complete the set. interval.Iterator's plain seeks now clear an overlap scan in progress, so NextOverlap after a seek invalidates instead of continuing with stale constraints; tested. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The generic monoid updater made three interface calls per level for what the count augmentation needs as an integer increment, costing orderstat 15% on inserts against the plain map. MonoidUpdater now returns a dedicated Updater for aug.Count with no interface calls; Prefix, SeekWhere and the rest keep working through the same monoid hook. orderstat is back to within 5% of the plain map. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Searching a node now touches only keys. Against the interleaved layout, Get is 5-9% faster with 8-byte values, 22-31% with 64-byte values and 20-24% with 256-byte values; delete+insert gains 3-21% with the same sizes. The clone-heavy round with pointer values pays 1-4% for copying two slices per copied node. Sets are unaffected: a zero-size value slice costs no allocation. BenchmarkValueSize records the measurement. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
TestNoStaleReferences uses weak pointers to prove that values removed by top-down Delete, Cursor.Delete, replacement and Clear (of a clone and of the original) become unreachable, so no stale slot in a live or free-listed node retains them. TestPointerTypesUnderGCPressure runs a mixed workload with string keys and pointer values, clones and cursor deletes while forcing collections, under the race detector. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e total, cheaper absent deletes - New rejects degrees above MaxDegree (16384): positions are int16, and a larger degree made Count wrap so iterators saw nothing while Get and Len worked. Tested at the cap. - Verify no longer panics for interface-typed keys or values: a nil interface gives reflect the invalid Value, which IsZero rejects. - SeekWhere returns the total when it ends past the end inside a subtree, as its documentation promised; the non-monotone test asserts it. - Delete of an absent key from a shared tree checked existence first, so it no longer copies the whole path; a counting free list verifies no node is allocated. Only the first write after a Clone pays the check. - Documented that Monoid.Combine must be commutative (the Updater folds contributions in operation order) and that Replacement's key may differ for an in-place Cursor.Rekey while still sorting between the same neighbours; tidied a shadowed variable in Cursor.Delete. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The check made deleting an absent key from a fresh clone free, but cost every first delete after a Clone a second descent: the clone-then-delete benchmark regressed 16-20%. Deleting present keys is the common case, so Delete goes back to one mutating descent and documents that an absent key on a shared tree copies the path, as google/btree does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… Verify never writes shared nodes - Sum takes Integer types only: floating-point sums are neither associative nor invertible, so they cannot be a Monoid, let alone a Group, and Verify would report drift on a correct tree. - MaxDegree is math.MaxInt16/2 so that the interval scan's Count()+1 sentinel fits in an int16 on a full leaf. - Verify recomputes augmentations on a shallow copy of each node, so it is safe alongside readers of snapshots sharing those nodes. - Range and Aggregate skip the lower boundary child when lo is found in the node, since every key in it is below lo. - The bounded free list allocates its slice on the first Put instead of at construction; the iterator frame stack clears popped slots, returns to the inline array on reset and reuses a spilled slice; Next and Cursor.Delete share one settle helper. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Total, Prefix and Aggregate, and Prefix and SeekWhere on iterators and cursors, were promoted onto every map built on aug, including btree.Map and interval.Map, where they could only panic. They now live on aug.MonoidMap, MonoidIterator and MonoidCursor, which NewMonoid and NewOrderedMonoid return; orderstat is built on MonoidMap. A plain map no longer offers them at all. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rection btree.Map held a pointer to aug.Map and orderstat.Map a pointer to aug.MonoidMap which held a pointer to aug.Map: two or three allocations per New and Clone and an indirection per method call. aug.MonoidMap now embeds Map by value, and the btree, orderstat and interval maps and sets are defined types over the aug types with explicit forwarding methods, so the conversion is free and each map is a single allocation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.
Summary
A rewrite of the core with the same design, driven by the fit-index brief: google/btree-class copy-on-write behaviour, pluggable free lists, order-statistic queries, and a mutating cursor, on Go 1.26 with no dependencies.
CHANGELOG.mdlists the API changes;docs/design.mdexplains the architecture;docs/review-2026-09-29.mdis the log of every measurement and decision.Bugs fixed
Clonedropped every value in a node on its first copy-on-write (the library had only been exercised as a set).Rank()was wrong for trees of height 3 or more.Nextpast the end andPrevbefore the beginning panicked on interior roots.Changes
internal/abstractbecomes the public packageaug. Nodes are slice-backed with keys and values in separate slices; the degree is a per-tree option (default 16) and free lists are pluggable (bounded default of 256,sync.Poolvariant).New/NewSetreturn pointers;NewOrderedsearches nodes with<forcmp.Orderedkeys.ResetisClear.Monoid,Group,Count,Sum,PairOf) withTotal,Prefix(key),Aggregate(lo, hi)andSeekWhere(pred)onMonoidMap;UpdateInfocarries values and aReplacementaction.Count:Rank(key),Nth,Count(lo, hi),Cursor.Cursor:SetValue,Delete,Rekey, hintedUpsert, in place when the leaf allows it, maintaining augmentations up the pinned path.SeekLE/SeekGT;All/Backward/Range/Fromviews;Min/Max;interval.Bounds,BoundsOf,Overlapping.Map.Verifyinvariant checker; differential, property, fuzz, retention and race tests across degrees 2 to 64; CI with gofmt, vet, staticcheck, race tests, fuzzing and govulncheck.Performance (Apple M-series, Go 1.27.1, three runs, vs google/btree v1.1.3 at equal degree)
NewOrderedExperiments run and decided on evidence (all in the review doc): separate key/value layout (adopted), ordered-key search (adopted), degree 16 (confirmed), free-list size, native
SeekNth, non-atomic refcounts (declined), eager cursor prefix (declined), an existence check inDelete(reverted after a regression).Test plan
go test -race ./...on Go 1.26.8 and 1.27.1go vet, staticcheck, govulncheck cleanFuzzMapandFuzzCursor🤖 Generated with Claude Code