Skip to content

b+tree: state the bounds a node actually has - #155

Open
psiha wants to merge 1 commit into
masterfrom
bt/3-node-bounds
Open

b+tree: state the bounds a node actually has#155
psiha wants to merge 1 commit into
masterfrom
bt/3-node-bounds

Conversation

@psiha

@psiha psiha commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Two places took a bound on faith.

The minimum fill is not an arbitrary half. min_values = ceil( max / 2 ) is exactly what makes 2 * min <= max + 1 true, which is what makes "either a sibling can lend a value, or the two merge into one node" true — the property the whole underflow half rests on (handle_underflow, merge_right_into_left, append_and_free, and the bulk-fill partitions written as min_values * 2). Raising the minimum past it does not merely make those suboptimal: it makes the 2-into-1 merge overflow the node silently, before it assertsmove_chldrn bounds count and tgt_begin separately and never their sum. Now a static_assert, so a higher fill target has to bring its own merge shape rather than discover this at runtime.

Intra-node search used the leaf's capacity while also running on inner nodes — for both the bound assumed on num_vals and the compile-time choice between a linear scan and a binary search. For a set the leaf is the larger of the two, so the assumption held and the dispatch was merely conservative. Neither survives a leaf that carries anything besides the key: the leaf then holds fewer entries than the inner node, the assumption becomes false for a full inner node — an assumption, not an assertion, so a release build miscompiles rather than trips — and the dispatch would drag the larger node onto the linear path, past the byte limit that path was measured against.

Capacity is now a parameter, taken from whichever node is actually being searched.

Stacked on #154.

Two places took a bound on faith.

The minimum fill is not an arbitrary half: 'min_values = ceil( max / 2 )' is
exactly what makes '2 * min <= max + 1' true, which is what makes 'either a
sibling can lend a value, or the two merge into one node' true - the property
the whole underflow half rests on (handle_underflow, merge_right_into_left,
append_and_free, and the bulk-fill partitions written as 'min_values * 2').
Raising the minimum past it does not merely make those suboptimal, it makes
the 2-into-1 merge overflow the node silently, before it asserts.  Now a
static_assert, so a higher fill target has to bring its own merge shape.

The intra-node lower_bound/upper_bound took the LEAF's capacity for both the
bound asserted on num_vals and the compile-time choice between a linear scan
and a binary search - while also being called on inner nodes.  For a set the
leaf is the larger of the two, so the assumption held and the dispatch was
merely conservative.  Neither survives a leaf that carries anything besides
the key: the leaf then holds fewer entries than the inner node, the assumption
becomes false for a full inner node - an assumption, not an assertion, so a
release build miscompiles rather than trips - and the dispatch would drag the
larger node onto the linear path, past the byte limit it was measured against.
Capacity is now a parameter, taken from whichever node is being searched.
Base automatically changed from bt/2-lower-bound to master September 9, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant