|
| 1 | +# Bitmap-Indexed Coalescing Range |
| 2 | + |
| 3 | +## The problem |
| 4 | + |
| 5 | +snmalloc's `LargeBuddyRange` only stores power-of-two blocks. A request for 5 |
| 6 | +chunks must be served from an 8-chunk buddy block, wasting 3 chunks. We want |
| 7 | +to store blocks at their actual size and use snmalloc's full size class |
| 8 | +sequence at the range level. |
| 9 | + |
| 10 | +## The core idea: search upward, skip a mask |
| 11 | + |
| 12 | +Free blocks are binned by the set of size classes they can serve. To allocate, |
| 13 | +search upward through bins — any larger block can be carved down. This almost |
| 14 | +works perfectly, but some bins hold blocks whose alignment is too poor to |
| 15 | +serve certain smaller, more-aligned sizes. Those bins must be masked out |
| 16 | +during the search. |
| 17 | + |
| 18 | +The mechanism: `find_first_set(bitmap & ~skip_mask)`. The skip mask depends |
| 19 | +only on the requested size class, not on the block. It's a small constant |
| 20 | +that can be precomputed. |
| 21 | + |
| 22 | +## Why skips exist |
| 23 | + |
| 24 | +snmalloc's size classes follow `S = 2^e + m · 2^(e−B)`, where `B` is the |
| 25 | +number of intermediate bits. Each size class has a natural alignment |
| 26 | +`align(S) = S & ~(S−1)`. |
| 27 | + |
| 28 | +A size class with high alignment needs padding to reach an aligned address |
| 29 | +within a block. A block of a *larger* size class with *lower* alignment may |
| 30 | +not have room for that padding. Concretely: a block of size 5 at address 1 |
| 31 | +can serve size 5 (alignment 1) but cannot serve size 4 (alignment 4) — there |
| 32 | +aren't enough chunks left after padding to the first 4-aligned address. |
| 33 | + |
| 34 | +Same size block, different address, different capability. This is what creates |
| 35 | +the need for separate bins and skip masks. |
| 36 | + |
| 37 | +## The general structure |
| 38 | + |
| 39 | +At each exponent level, the distinct "servable sets" (which size classes a |
| 40 | +block can serve) form a structure with some incomparable pairs. Exhaustive |
| 41 | +enumeration shows: |
| 42 | + |
| 43 | +| B | Mantissas/exponent | Bins/exponent | Max skip mask bits | |
| 44 | +|---|-------------------:|--------------:|-------------------:| |
| 45 | +| 1 | 2 | 2 | 0 | |
| 46 | +| 2 | 4 | 5 | 1 | |
| 47 | +| 3 | 8 | 13 | 4 | |
| 48 | +| 4 | 16 | 34 | 11 | |
| 49 | + |
| 50 | +Each bin corresponds to a distinct servable set. The bins are ordered so that |
| 51 | +upward search is almost always correct — the skip mask handles the exceptions. |
| 52 | + |
| 53 | +For any B, the structure is: |
| 54 | +- **Most requests need no skips.** Only size classes with alignment higher |
| 55 | + than expected for their position in the sequence need to mask anything. |
| 56 | +- **The skip mask is a small constant** per size class, precomputable at |
| 57 | + compile time. |
| 58 | +- **The mechanism is identical** regardless of B: |
| 59 | + `find_first_set(bitmap & ~skip_mask, start_bit)`. |
| 60 | + |
| 61 | +`prototype/skip_analysis.py` verifies this exhaustively for B = 1, 2, 3. |
| 62 | + |
| 63 | +## The bitmap design |
| 64 | + |
| 65 | +Each free block gets one bin based on its size and alignment. Within each |
| 66 | +exponent, there are as many bins as there are distinct servable sets (5 for |
| 67 | +B=2, 13 for B=3). A flat bitmap tracks which bins are non-empty. |
| 68 | + |
| 69 | +To allocate size class `(e, m)`: |
| 70 | + |
| 71 | +1. Compute the **start bit** — the first bin that could serve this size class. |
| 72 | +2. Compute the **skip mask** — bits for bins that can't serve this request. |
| 73 | +3. `find_first_set(bitmap & ~skip_mask, start_bit)` → pop a block from that |
| 74 | + bin. |
| 75 | + |
| 76 | +The returned block may not be exactly aligned for the requested size class. |
| 77 | +The caller **carves** the aligned region and returns any prefix/suffix |
| 78 | +remainders to the free pool. |
| 79 | + |
| 80 | +## Contrast with buddy allocators |
| 81 | + |
| 82 | +A buddy allocator guarantees alignment by construction — a 16-chunk buddy is |
| 83 | +always 16-aligned — but wastes space by decomposing everything into |
| 84 | +power-of-two pieces. |
| 85 | + |
| 86 | +This design stores blocks at their actual size (no decomposition, no waste) |
| 87 | +and handles alignment at allocation time by carving. The skip mask makes |
| 88 | +lookup O(1) despite blocks having arbitrary size and alignment. |
| 89 | + |
| 90 | +## Concrete example (B = 2) |
| 91 | + |
| 92 | +At exponent `e = 2`, the size classes are 4, 5, 6, 7. There are 5 bins, |
| 93 | +each labeled by the set of size classes it can serve at this exponent: |
| 94 | + |
| 95 | + Bin 0: serves {4} |
| 96 | + Bin 1: serves {5} |
| 97 | + Bin 2: serves {4, 5} |
| 98 | + Bin 3: serves {4, 5, 6} |
| 99 | + Bin 4: serves {4, 5, 6, 7} |
| 100 | + |
| 101 | +Allocation searches upward from the smallest sufficient bin: |
| 102 | + |
| 103 | + Request for 7: can use bin 4 → search bits {4} |
| 104 | + Request for 6: can use bins 3, 4 → search bits {3, 4} |
| 105 | + Request for 5: can use bins 1, 2, 3, 4 → search bits {1, 2, 3, 4} |
| 106 | + Request for 4: can use bins 0, 2, 3, 4 — skip 1 → search bits {0, 2, 3, 4} |
| 107 | + |
| 108 | +Only the request for size 4 needs to skip a bin: bin 1 holds blocks that can |
| 109 | +serve 5 but not 4. The skip mask is just bit 1. |
| 110 | + |
| 111 | +## Concrete example (B = 3) |
| 112 | + |
| 113 | +At exponent `e = 4`, the size classes are 16, 18, 20, 22, 24, 26, 28, 30. |
| 114 | +There are 13 bins. The skip analysis shows: |
| 115 | + |
| 116 | + Request for 16 (align 16): must skip bins for {18}, {20}, {22}, {26} |
| 117 | + Request for 24 (align 8): must skip bin for {26} |
| 118 | + All other requests: no skips needed |
| 119 | + |
| 120 | +The pattern: size 16 has high alignment and must skip 4 bins whose blocks |
| 121 | +are large enough but too poorly aligned. Size 24 is a "sub-power-of-two" |
| 122 | +(alignment 8) and must skip 1 bin. All odd-coefficient sizes have low |
| 123 | +alignment and never need to skip anything. |
| 124 | + |
| 125 | +Same mechanism, wider mask, same `find_first_set(bitmap & ~mask)` operation. |
0 commit comments