pub struct SizeClasses<const N: usize, const L: usize> { /* private fields */ }Expand description
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.
N— the number of classes (geo_count + extras.len()).L— thesize2classlength (size2class_len(max_class, min_block)).
Construct one at compile time with SizeClasses::build. All query methods
are const pure arithmetic — no allocation, and no panics on the lookup
path FOR IN-CONTRACT INPUTS: need = max(size, align) >= 1 (so size == 0 alone is fine whenever align >= 1 – see
class_for’s own doc for the precise domain), a
power-of-two align, and an idx obtained from
class_for rather than picked independently — an
out-of-range idx does panic, see block_size.
Deliberately not Copy: duplicating a realistic scheme is ~16 KiB (see
size2class_len’s # Memory cost for the breakdown), so call
.clone() explicitly. Intended use is a static referenced in place;
no method needs ownership. (Design rationale: the CHANGELOG.)
Debug prints a short summary, not the raw tables – inspect those with
table / size2class.
Implementations§
Source§impl<const N: usize, const L: usize> SizeClasses<N, L>
impl<const N: usize, const L: usize> SizeClasses<N, L>
Sourcepub const fn build(params: Params<'_>) -> Self
pub const fn build(params: Params<'_>) -> Self
Build a scheme from Params at compile time. N and L must match
the params (see build_table / build_size2class for the exact
obligations); a mismatch panics identically in const evaluation
(compile error) and at runtime.
Intended placement is a static, not a const: both const-evaluate
this for free, but a const item re-materializes its value at every
use site, duplicating the embedded tables, while a static is one
fixed-address copy referenced in place. Nothing stops calling it at
runtime instead of either: doing so materializes the whole return
value – at least L bytes, several KiB for a realistic scheme – by
value on the caller’s stack, which matters on a small-stack no_std
target.
small_align_max — the alignment ceiling of the O(1) fast path — is set
to min_block: every class size is a multiple of min_block, so the
stride trivially satisfies divisibility for any align <= min_block
(see class_for’s # Preconditions for the
separate base-address requirement). Larger alignments take the
divisibility-jump slow path in
class_for.
Sourcepub const fn table(&self) -> &[usize; N]
pub const fn table(&self) -> &[usize; N]
The class table (strictly increasing, each entry a multiple of
min_block). The single source of truth for the scheme’s geometry.
Sourcepub const fn size2class(&self) -> &[u8; L]
pub const fn size2class(&self) -> &[u8; L]
The derived O(1) size→class lookup, as built by build_size2class
– see that function’s doc for the indexing formula and the L - 1
top-bucket clamp.
LOW-LEVEL: unlike class_for, this accessor does
not itself validate a raw caller’s size. The documented formula is
size2class()[(size - 1) >> min_block_shift()], which has TWO
preconditions this array does not enforce:
size >= 1—size - 1underflows forsize == 0; guard it withsize.checked_sub(1)ifsizemay be0.size <= small_max()for a genuine classification. Do NOT derive this bound as a byte size (L * min_block()is NOT guaranteed to fitusizefor every valid scheme). Comparesizetosmall_maxdirectly, or reason about the INDEX instead — beyondsmall_max()the raw index is NOT uniformly clamped:idx == L - 1is in-bounds and returns the clamped sentinel (a false “fits” instead of theNoneclass_forwould give), whileidx >= Lis genuinely out-of-bounds and panics.
class_for avoids both pitfalls (its need = max(size, align) is always >= 1, and it rejects a too-large need
before indexing) and additionally applies the align predicate this
raw LUT ignores. Prefer it unless you specifically need the raw LUT.
This shape is a deliberate, but not permanently promised, choice.
The LUT is today one flat u8 per min_block-sized bucket over the
whole size range (see size2class_len’s # Memory cost) — the
simplest shape that stays O(1) for arbitrary extras, but a
memory-hungry one for a scheme with a large max_class. L being a
public const generic means a future layout change (e.g. a hybrid:
an exact small-size LUT below some threshold, a computed answer
above it) would very likely require a breaking release regardless.
Sourcepub const fn min_block(&self) -> usize
pub const fn min_block(&self) -> usize
The minimum block size / fundamental alignment (min_block).
Derived from min_block_shift (1 << min_block_shift) rather than stored separately – the two are equal
by construction (see build).
Sourcepub const fn min_block_shift(&self) -> u32
pub const fn min_block_shift(&self) -> u32
log2(min_block) — the shift turning a byte size into a
min_block-unit index.
Sourcepub const fn small_align_max(&self) -> usize
pub const fn small_align_max(&self) -> usize
The alignment ceiling of the O(1) fast path (equal to min_block) –
not the ceiling on alignments class_for can
serve at all; larger alignments take its slow path instead.
Sourcepub const fn small_max(&self) -> usize
pub const fn small_max(&self) -> usize
The largest class (table[N - 1]). A request larger than this — or with
an alignment larger than this — takes the caller’s large path.
Sourcepub const fn block_size(&self, idx: usize) -> usize
pub const fn block_size(&self, idx: usize) -> usize
Sourcepub const fn huge_threshold(&self) -> usize
pub const fn huge_threshold(&self) -> usize
The caller’s Params::huge_threshold policy value, as built. The
only Params field with a dedicated read-back accessor here, for a
caller that needs to report or log the threshold without keeping its
own separate copy of it.
Sourcepub const fn is_huge(&self, size: usize) -> bool
pub const fn is_huge(&self, size: usize) -> bool
Whether a size request is “huge” per the caller’s
Params::huge_threshold policy.
Sourcepub const fn class_for(&self, size: usize, align: usize) -> Option<usize>
pub const fn class_for(&self, size: usize, align: usize) -> Option<usize>
Resolve (size, align) to a class index, or None for the caller’s
large path.
A class fits iff its block_size >= max(size, align) AND
block_size % align == 0. Returns the index of the smallest such class.
The divisibility conjunct is a STRIDE property, not an address
guarantee — see # Preconditions below for what it does and does not
establish about block addresses.
Fast path (align <= min_block): every class SIZE is a multiple of
min_block, which does two things: the stride divisibility check is
trivially satisfied, and the LUT’s bucket-top answer is the
smallest fitting class (no class value can lie strictly between
need and its bucket’s top) — one O(1) lookup (same base-alignment
precondition as the slow path, below).
Slow path (align > min_block, a power of two): seed at the lookup
entry covering max(size, align), then jump forward over non-divisible
classes — from a non-divisible class of block size b, the next class
that could be a multiple of align is the one covering the smallest
multiple of align strictly greater than b (a bitmask round-up plus
one lookup). Provably equivalent to a step-by-1 walk, never more
iterations, fewer whenever the jump skips at least one class.
The useful domain is size >= 1; more precisely, what must hold is
need = max(size, align) >= 1, so size == 0 alone is fine whenever
align >= 1 – (need - 1) >> shift never underflows in that case.
Consumers commonly clamp size up to min_block before calling (the
classifier has no smaller class to offer below it anyway); this
function does not require that clamp.
§Preconditions
align must be a power of two — the same Layout contract the
standard allocator API requires. An align taken from
core::alloc::Layout satisfies this by construction; one computed
by hand may not.
A violation trips a debug_assert! whenever cfg(debug_assertions) is
on (both this and the overflow-checks knob below track the profile
size-classes itself is compiled with, which normally tracks the
consumer’s). With debug_assertions off, the behavior for ANY
non-power-of-two align – including 0 – is UNSPECIFIED: an
incorrect Some/None (the fast path skips the divisibility check
entirely; the slow path’s bitmask round-up and its
block & (align - 1) == 0 test both assume a
power of two and can overshoot, under-return, or wrongly accept a
non-fitting class), never memory unsafety or a corrupt table. The
one non-power-of-two input with a SPECIFIED outcome is the
align == 0, size == 0 corner, which does NOT panic, but only with
overflow-checks ALSO off (a separate Cargo knob from
debug_assertions): need - 1 underflows to usize::MAX, landing on
the same early None any other out-of-range request takes (see
class_for’s own index-space guard comment for the proof); with
overflow-checks on, that subtraction panics instead. Prefer
try_class_for, which rejects align == 0
before any of this arithmetic runs in every profile, over relying on
this fallback behavior.
The carve base must also be align-aligned. For blocks carved at
base + k * block_size, block_size % align == 0 gives address(k) % align == base % align for every k: the stride PRESERVES whatever
alignment the carve base already has (so no per-block padding is ever
needed) — it cannot CREATE alignment the base lacks. This crate
computes over sizes only and never sees an address, so it CANNOT
check this — unlike the power-of-two contract above, it is not even
debug_assert-able here. The caller must place block 0 of the run
serving a returned class at an address base with base % align == 0 for every align it resolves through this scheme (the address
that matters is block 0’s, not the span’s OS reservation base, if
the two differ). Carving every run from a base whose power-of-two
alignment is >= the largest align the scheme will ever serve
satisfies this for every smaller align too.
A violation cannot corrupt this crate’s own scheme or cause UB INSIDE
IT (pure arithmetic over sizes, no addresses touched) — it yields
blocks whose SIZE is align-divisible but whose ADDRESSES are all
congruent to the same base % align != 0. But an allocator built on
top of this crate that returns such a misaligned pointer for a
request with that align violates ITS OWN Layout contract with its
caller — the downstream consequence is safety-critical even though
this crate cannot detect or cause it directly.
Sourcepub const fn try_class_for(
&self,
size: usize,
align: usize,
) -> Result<Option<usize>, InvalidAlign>
pub const fn try_class_for( &self, size: usize, align: usize, ) -> Result<Option<usize>, InvalidAlign>
The checked twin of class_for: validates align
instead of assuming it (Err(InvalidAlign) for a non-power-of-two
align, including 0), then delegates. Same result on every
already-valid input; the only behavior difference is on the inputs
class_for’s own # Preconditions already document as
contract-violating. Does strictly more work than class_for (the
added power-of-two check).
Never panics, for any (size, align) pair — this is the
substantive reason to prefer it over class_for for an align that
is not already known-valid: a non-power-of-two align (including
0) is rejected before any arithmetic runs, so need = max(size, align) is always >= 1 past that point; the seed index
(need - 1) >> min_block_shift is compared against the compile-time
bound L - 1 before any indexing, so both the seed and every
slow-path re-seed stay strictly inside size2class(), and the
slow-path jump loop is bounded exactly as class_for’s own doc
proves.
Use this one unless align is already known-valid by construction
(e.g. taken directly from a core::alloc::Layout) – class_for
stays the zero-validation hot-path variant for that case, matching
Layout::from_size_align (checked) versus
Layout::from_size_align_unchecked (trusted) in core::alloc.