Skip to content

Commit f4e3692

Browse files
g-andradeclaude
andcommitted
Document bulk construction
The formula behind from_ordset/from_orddict/from_ordered_list was never written down, so record what it computes and why. xb5_utils:bulk_construction_params/1 counts in base 4 because bulk construction targets nodes of 3 keys and 4 children, not the maximum 4 and 5 - the density the README argues for under "Key density". From that: - BatchSize - 1 is 4^(h+1) - 1, the size of a perfect subtree of 3-key, 4-child nodes, handed to each fully loaded child. - BatchOffset is (4^(h+1) - 1) / 3, at once that subtree's node count and the smallest S reaching this level. The two coincide only because the target density is exactly 3. Confirmed against structural_stats: sizes of 4^(h+1) - 1 give exactly 3.000 keys per node over (4^(h+1) - 1) / 3 nodes, and the worst cases are the BatchOffsets themselves, where a level is forced and the root is left an INTERNAL1. Document the consuming half above xb5_sets_node:from_ordset_recur/5 - the quotient picking each node's arity, INTERNAL1 staying root-only as deletion also requires, and the tail split. The three-quarter cap in from_ordset_right_children_sizes/2 is recorded as tuned rather than derived, which is what it is. xb5_trees_node and xb5_bag_node point at both places instead of repeating them. Comments only, no change to compiled behaviour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4b83c7d commit f4e3692

4 files changed

Lines changed: 68 additions & 0 deletions

File tree

src/xb5_bag_node.erl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,6 +1450,13 @@ fold_recur(Fun, Acc, Node) ->
14501450
%% Internal Function Definitions: from_ordered_list/2
14511451
%% ------------------------------------------------------------------
14521452

1453+
%% The bulk construction scheme is documented at
1454+
%% xb5_utils:bulk_construction_params/1 (what BatchOffset and BatchSize mean)
1455+
%% and above xb5_sets_node:from_ordset_recur/5 (how they pick each node's
1456+
%% arity). What follows is that same algorithm, additionally deriving each
1457+
%% node's offsets O1..On straight from the child sizes S1..Sn - which this
1458+
%% scheme settles before any child is built, so they cost nothing to obtain.
1459+
14531460
from_ordered_list_recur(S, L, BatchOffset, BatchSize, AtRoot) when S >= 5 ->
14541461
ChildrenBatchOffset = BatchOffset - BatchSize,
14551462
ChildrenBatchSize = BatchSize bsr 2,

src/xb5_sets_node.erl

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,33 @@ fold_recur(Fun, Acc, Node) ->
14041404
%% Internal Function Definitions: from_ordset/2
14051405
%% ------------------------------------------------------------------
14061406

1407+
%% Builds the tree bottom-up in one pass over an already-ordered list, taking
1408+
%% the elements in order and never comparing them. What BatchOffset and
1409+
%% BatchSize mean is documented at xb5_utils:bulk_construction_params/1; in
1410+
%% short, BatchSize - 1 is the size of a perfect subtree of 3-key, 4-child
1411+
%% nodes at this level, and BatchOffset the smallest S that reaches it.
1412+
%%
1413+
%% Given those, (S - BatchOffset) div BatchSize is how many children can be
1414+
%% handed a full BatchSize - 1 elements, which settles this node's arity:
1415+
%%
1416+
%% 0 (at root) INTERNAL1, 1 key 0 full children + 2 sized from the tail
1417+
%% 0 (deeper) INTERNAL2, 2 keys ... treated as 1, see below
1418+
%% 1 INTERNAL2, 2 keys 1 full child + 2 sized from the tail
1419+
%% 2 INTERNAL3, 3 keys 2 full + 2
1420+
%% 3 INTERNAL4, 4 keys 3 full + 2
1421+
%%
1422+
%% A quotient of 0 only yields an INTERNAL1 at the root, which is the sole
1423+
%% place a 1-key node may come to rest - the same invariant deletion upholds.
1424+
%% Below the root a 0 is folded in with 1 and built as an INTERNAL2 instead,
1425+
%% borrowing from the tail to fill it.
1426+
%%
1427+
%% Whatever the arity, the last two children are always sized by
1428+
%% from_ordset_right_children_sizes/2 rather than filled: it takes the
1429+
%% remaining elements and splits them, reserving one for the key between the
1430+
%% two. When that remainder is long enough for both to take a full batch it
1431+
%% gives the left one BatchSize - 1; when it is not, it caps the left at
1432+
%% three quarters of a batch, so that what is left over does not strand the
1433+
%% right child nearly empty. The three quarters is tuned rather than derived.
14071434
from_ordset_recur(S, L, BatchOffset, BatchSize, AtRoot) when S >= 5 ->
14081435
ChildrenBatchOffset = BatchOffset - BatchSize,
14091436
ChildrenBatchSize = BatchSize bsr 2,

src/xb5_trees_node.erl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,11 @@ foldr_recur(Fun, Acc, Node) ->
14611461
%% Internal Function Definitions: from_orddict/2
14621462
%% ------------------------------------------------------------------
14631463

1464+
%% The bulk construction scheme is documented at
1465+
%% xb5_utils:bulk_construction_params/1 (what BatchOffset and BatchSize mean)
1466+
%% and above xb5_sets_node:from_ordset_recur/5 (how they pick each node's
1467+
%% arity). What follows is that same algorithm over key/value pairs.
1468+
14641469
from_orddict_recur(S, L, BatchOffset, BatchSize, AtRoot) when S >= 5 ->
14651470
ChildrenBatchOffset = BatchOffset - BatchSize,
14661471
ChildrenBatchSize = BatchSize bsr 2,

src/xb5_utils.erl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,35 @@
2121
dialyzer_opaque_term(V) ->
2222
V.
2323

24+
%% Seeds bulk construction of a tree of `S' elements from already-ordered
25+
%% input: xb5_sets_node:from_ordset/2, xb5_trees_node:from_orddict/2 and
26+
%% xb5_bag_node:from_ordered_list/2 all start here, then recurse with the
27+
%% pair returned.
28+
%%
29+
%% The arithmetic counts in base 4, because bulk construction aims for nodes
30+
%% of 3 keys and 4 children rather than the maximum 4 and 5 - the key density
31+
%% the README argues for under "Key density". Writing the levels out:
32+
%%
33+
%% S range BatchOffset BatchSize BatchSize - 1
34+
%% < 5 1 1 0
35+
%% 5 .. 20 5 4 3
36+
%% 21 .. 84 21 16 15
37+
%% 85 .. 340 85 64 63
38+
%% 341 .. 341 256 255
39+
%%
40+
%% `BatchSize - 1' is 4^(h+1) - 1, the size of a perfect subtree of height h
41+
%% in which every node holds 3 keys and 4 children. It is the size handed to
42+
%% each fully loaded child.
43+
%%
44+
%% `BatchOffset' is (4^(h+1) - 1) / 3, which is at once the node count of that
45+
%% perfect subtree and the smallest `S' for which this level is chosen. The
46+
%% two coincide only because the target density is exactly 3.
47+
%%
48+
%% So the loop below returns the deepest level whose perfect subtree still
49+
%% fits inside `S'. Feeding it exactly 4^(h+1) - 1 elements yields a tree of
50+
%% precisely 3.000 keys per node and (4^(h+1) - 1) / 3 nodes; the worst case
51+
%% is `S' equal to a BatchOffset, the point at which a new level is forced and
52+
%% the root is left an INTERNAL1.
2453
-spec bulk_construction_params(non_neg_integer()) ->
2554
nonempty_improper_list(BatchOffset, BatchSize)
2655
when

0 commit comments

Comments
 (0)