Skip to main content

SizeClasses

Struct SizeClasses 

Source
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 — the size2class length (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>

Source

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.

Source

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.

Source

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 >= 1size - 1 underflows for size == 0; guard it with size.checked_sub(1) if size may be 0.
  • size <= small_max() for a genuine classification. Do NOT derive this bound as a byte size (L * min_block() is NOT guaranteed to fit usize for every valid scheme). Compare size to small_max directly, or reason about the INDEX instead — beyond small_max() the raw index is NOT uniformly clamped: idx == L - 1 is in-bounds and returns the clamped sentinel (a false “fits” instead of the None class_for would give), while idx >= L is 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.

Source

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).

Source

pub const fn min_block_shift(&self) -> u32

log2(min_block) — the shift turning a byte size into a min_block-unit index.

Source

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.

Source

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.

Source

pub const fn count(&self) -> usize

The number of classes (N).

Source

pub const fn block_size(&self, idx: usize) -> usize

The block size of class idx.

§Panics

Panics if idx >= N — the caller only ever passes indices returned by class_for.

Source

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.

Source

pub const fn is_huge(&self, size: usize) -> bool

Whether a size request is “huge” per the caller’s Params::huge_threshold policy.

Source

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.

Source

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.

Trait Implementations§

Source§

impl<const N: usize, const L: usize> Clone for SizeClasses<N, L>

Source§

fn clone(&self) -> SizeClasses<N, L>

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl<const N: usize, const L: usize> Debug for SizeClasses<N, L>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl<const N: usize, const L: usize> Freeze for SizeClasses<N, L>
where [usize; N]: Freeze, [u8; L]: Freeze,

§

impl<const N: usize, const L: usize> RefUnwindSafe for SizeClasses<N, L>

§

impl<const N: usize, const L: usize> Send for SizeClasses<N, L>
where [usize; N]: Send, [u8; L]: Send,

§

impl<const N: usize, const L: usize> Sync for SizeClasses<N, L>
where [usize; N]: Sync, [u8; L]: Sync,

§

impl<const N: usize, const L: usize> Unpin for SizeClasses<N, L>
where [usize; N]: Unpin, [u8; L]: Unpin,

§

impl<const N: usize, const L: usize> UnsafeUnpin for SizeClasses<N, L>
where [usize; N]: UnsafeUnpin, [u8; L]: UnsafeUnpin,

§

impl<const N: usize, const L: usize> UnwindSafe for SizeClasses<N, L>
where [usize; N]: UnwindSafe, [u8; L]: UnwindSafe,

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.