Skip to main content

Crate size_classes

Crate size_classes 

Source
Expand description

size-classes — const-built size-class tables + a compile-time-derived O(1) size→class lookup + an alignment-divisibility classifier.

Every slab / pool / arena allocator reinvents the same trio: a table of block sizes, an O(1) map from a requested byte size to the smallest class that fits it, and a classifier that also honours alignment via stride divisibility. This crate packages that trio as a const-evaluated, no_std, zero-dependency, #![forbid(unsafe_code)] unit — the table shape is a parameter, so a consumer can bake its own scheme and still get the derived lookup and the alignment-aware classifier for free.

§The three pieces

  • build_table — a const fn sorted-merge of a geometric progression (geo_count classes, each round_up(ceil(prev * num / den), min_block)) with a strictly increasing, min_block-multiple, >= min_block list of explicit extras (page-aligned classes, an exact size the geometric run skips, a feature-gated medium tier, …).
  • build_size2class — derives the O(1) size→class lookup from a table at compile time with the monotone-pointer technique (O(buckets + classes) const-eval) and a compile-time u8 pin.
  • SizeClasses::class_for — an O(1) fast path for align <= min_block and a provably-equivalent jump slow path for larger alignments: round block up to the next multiple of align via a bitmask, re-seed through the lookup, and so skip whole runs of non-divisible classes instead of stepping by one. Without it, a request whose align exceeds what the caller’s classifier happens to handle silently falls through to the caller’s whole-segment path — a real bug class in hand-rolled allocators (sefer-alloc’s own motivating case, the allocator this crate was extracted from: align >= 512). The classifier picks an align-divisible stride; see SizeClasses::class_for’s # Preconditions for the separate base-address requirement this crate cannot check. SizeClasses::try_class_for is the checked twin – validates align instead of assuming it. Use it unless align is already known-valid by construction (e.g. taken from a core::alloc::Layout).

§The huge threshold is a policy parameter

SizeClasses::is_huge compares against a caller-supplied Params::huge_threshold. The crate has no notion of an OS segment size; the consumer picks the threshold that separates “large” from “huge” for its own segment policy.

§Deriving lengths

SizeClasses is generic over both the table length N (geo_count + extras.len()) and the lookup length L (max_class / min_block + 1, via size2class_len). Both are pure functions of the Params, but L needs the built table’s LAST entry (max_class) — there is no shortcut around building TABLE once to read it; SizeClasses::build then builds the same table again internally, from the same Params, so the two never drift apart:

const PARAMS: Params = Params::new(MIN_BLOCK, (5, 4), GEO_COUNT, EXTRAS, HUGE_THRESHOLD);
const N: usize = GEO_COUNT + EXTRAS.len();
const TABLE: [usize; N] = build_table::<N>(PARAMS);
const L: usize = size2class_len(TABLE[N - 1], MIN_BLOCK);
static SC: SizeClasses<N, L> = SizeClasses::build(PARAMS);

(Runnable form with concrete values in crates/size-classes/README.md.)

Structs§

InvalidAlign
The error SizeClasses::try_class_for returns when align is not a power of two (the core::alloc::Layout contract SizeClasses::class_for assumes but – on its own hot path – only debug_assert!s). Carries the offending value for diagnostics.
Params
Parameters for a size-class scheme, consumed by build_table, build_size2class and SizeClasses::build.
SizeClasses
A const-built size-class scheme: the sorted class table, its derived O(1) size→class lookup, and the policy constants needed to classify a request.

Functions§

build_size2class
Build the O(1) size→class lookup from a table at compile time — so the lookup and the table cannot drift. The caller indexes it as size2class[(size - 1) >> log2(min_block)], so bucket k covers every size in (k * min_block, (k + 1) * min_block]; size2class[k] is the smallest class whose block_size >= (k + 1) * min_block – EXCEPT the top bucket L - 1, whose ideal need (L * min_block mathematically – NOT guaranteed to fit usize even for a valid scheme, e.g. min_block = 1 << 62, L = 4; the builder computes it as (k + 1).checked_mul(min_block) and folds that same overflow into the clamp below, never evaluating the unrepresentable product) exceeds table[N - 1] (the largest class), so no such class exists; that bucket is clamped to table[N - 1] itself instead. For SizeClasses::class_for specifically this is harmless: it never queries bucket L - 1 for any in-range size (its own early-rejection guard catches every size that would land there), so THERE the clamped entry is an unreachable sentinel, not an observable answer. A caller driving this array directly (bypassing class_for) can still observe it – and for a hand-built table whose small_max is not a multiple of min_block, bucket L - 1 need not even be a sentinel: it can be the correct, reachable answer for sizes in ((L - 1) * min_block, small_max].
build_table
Build the size-class table at compile time: a geometric progression merged with params.extras in sorted order, returned as [usize; N] where N must equal params.geo_count + params.extras.len().
size2class_len
The size2class array length for a scheme whose largest class is max_class: one u8 per min_block-sized bucket from 0 up to and including max_class. A consumer uses this in a const expression to pin the L generic of SizeClasses.