Skip to content

Commit a3848f2

Browse files
committed
B+ trees
1 parent c6c5da4 commit a3848f2

File tree

2 files changed

+46
-22
lines changed

2 files changed

+46
-22
lines changed
32.9 KB
Loading

content/english/hpc/data-structures/s-tree.md

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ In this article, we generalize the techniques we developed for binary search to
1111
- The [first one](#b-tree-layout) is based on the memory layout of a B-tree, and, depending on the array size, it is up to 8x faster than `std::lower_bound` while using the same space as the array and only requiring a permutation of its elements.
1212
- The [second one](#b-tree-layout-1) is based on the memory layout of a B+ tree, and it is up to 15x faster that `std::lower_bound` while using just 6-7% more memory — or 6-7% **of** the memory if we can keep the original sorted array.
1313

14-
To distinguish them from B-trees — the structures with pointers, thousands to millions of elements per node, and empty spaces — we will use the names *S-tree* and *S+ tree* respectively to refer to these particular memory layouts[^name].
14+
To distinguish them from B-trees — the structures with pointers, hundreds to thousands of keys per node, and empty spaces in them — we will use the names *S-tree* and *S+ tree* respectively to refer to these particular memory layouts[^name].
1515

1616
[^name]: [Similar to B-trees](https://en.wikipedia.org/wiki/B-tree#Origin), "the more you think about what the S in S-trees means, the better you understand S-trees."
1717

@@ -310,25 +310,45 @@ To address these problems, we need to change the layout a little bit.
310310

311311
## B+ Tree Layout
312312

313-
The layout is not succinct: we need about some additional memory to store the internal nodes — about $\frac{1}{16}$-th of the original array size, to be exact.
313+
Most of the time people talk about B-trees they really mean *B+ trees*, which is a modification that distinguishes between the two types of nodes:
314+
315+
- *Internal nodes* store up to $B$ keys and $(B + 1)$ pointers to child nodes. The key number $i$ always equals the first key of of the $(i + 1)$-th child node.
316+
- *Data nodes* or *leaves* store up to $B$ keys, the pointer to the next leaf node, and, optionally, an associated value for each key, if the structure is used as a key-value map.
317+
318+
Advantages of this approach include faster search time as the internal nodes only store keys and the ability to quickly iterate over a range of entries by following next leaf node pointers, but this comes at the cost of some redundancy: we have to store copies of keys in the internal nodes.
319+
320+
![A B+ tree of order 4](../img/bplus.png)
321+
322+
Back to our use case, this layout can help us solve our two problems:
323+
324+
- Either the last node we descend into is has the local lower bound, or it is the first key of the next leaf node, so we don't need to call `update` on each iteration.
325+
- The depth of all leaves is constant because B+ trees grow at the root and not at the leaves, which removes the need for branching. <!-- todo: elaborate on that -->
326+
327+
The disadvantage is that this layout is not succinct: we need about some additional memory to store the internal nodes — about $\frac{1}{16}$-th of the original array size, to be exact — but the performance improvement will be more than worth it.
328+
329+
### Implicit B+ Tree
314330

315331
B-tree layout
316332

317333
We will explain the constexpr functions because this time it is important:
318334

319335
```c++
336+
// number of B-element blocks in a layer with n keys
320337
constexpr int blocks(int n) {
321338
return (n + B - 1) / B;
322339
}
323340

341+
// number of keys on the layer pervious to one with n element
324342
constexpr int prev_keys(int n) {
325343
return (blocks(n) + B) / (B + 1) * B;
326344
}
327345

346+
// height of a balanced n-key B+ tree
328347
constexpr int height(int n) {
329348
return (n <= B ? 1 : height(prev_keys(n)) + 1);
330349
}
331350

351+
// where each layer starts
332352
constexpr int offset(int h) {
333353
int k = 0, n = N;
334354
while (h--) {
@@ -341,35 +361,39 @@ constexpr int offset(int h) {
341361
const int H = height(N), S = offset(H);
342362
```
343363
344-
To be more explicit with pointer arithmetic, the tree is just a single array now:
364+
To be more explicit with pointer arithmetic, the tree is just a single huge-page aligned array `btree` of size `S`.
365+
366+
367+
We store in reverse order, but the nodes within a layer and data in them is still left-to-right. This is an arbitrary decision: you can do it the other way around, but it will be slightly harder to code.
345368
346369
```c++
347-
int *btree;
370+
memcpy(btree, a, 4 * N);
371+
372+
for (int i = N; i < S; i++)
373+
btree[i] = INT_MAX;
348374
```
349375

350376
```c++
351-
for (int i = N; i < S; i++)
352-
btree[i] = INT_MAX;
353-
354-
memcpy(btree, a, 4 * N);
355-
356-
for (int h = 1; h < H; h++) {
357-
for (int i = 0; i < offset(h + 1) - offset(h); i++) {
358-
int k = i / B,
359-
j = i - k * B;
360-
k = k * (B + 1) + j + 1; // compare right
361-
// and then always to the left
362-
for (int l = 0; l < h - 1; l++)
363-
k *= (B + 1);
364-
btree[offset(h) + i] = (k * B < N ? btree[k * B] : INT_MAX);
365-
}
377+
for (int h = 1; h < H; h++) {
378+
for (int i = 0; i < offset(h + 1) - offset(h); i++) {
379+
int k = i / B,
380+
j = i - k * B;
381+
k = k * (B + 1) + j + 1; // compare right
382+
// and then always to the left
383+
for (int l = 0; l < h - 1; l++)
384+
k *= (B + 1);
385+
btree[offset(h) + i] = (k * B < N ? btree[k * B] : INT_MAX);
366386
}
367-
368-
for (int i = offset(1); i < S; i += B)
369-
permute(btree + i);
370387
}
371388
```
372389

390+
```c++
391+
for (int i = offset(1); i < S; i += B)
392+
permute(btree + i);
393+
```
394+
395+
### Searching
396+
373397
```c++
374398
int lower_bound(int _x) {
375399
unsigned k = 0;

0 commit comments

Comments
 (0)