ealloc
A no_std memory allocator with pluggable free-block management strategies, built on ecore.
Unlike general-purpose allocators that make one-size-fits-all trade-offs, ealloc lets you fine-tune every aspect at compile time via type parameters — block size limits, alignment granularity, pointer width, and search strategy — so you can optimize for your specific application: minimize fragmentation, shrink metadata overhead, or maximize throughput, all without runtime cost. With metadata as low as single-digit bytes per free block, ealloc makes dynamic memory allocation practical even on MCUs with only a few kilobytes of SRAM.
Design Rationale: Why Not Standalone Allocation Algorithms?
Traditional allocators couple the "block-finding strategy" with "block splitting/coalescing," forcing each algorithm (TLSF, FirstFit, HalfTree…) to re-implement the same split-and-merge logic.
ealloc fully decouples these two concerns:
┌──────────────────────────────────────┐
│ LeanFlexAllocator │
│ ┌────────────────────────────────┐ │
│ │ Block splitting │ │
│ │ Block coalescing │ │
│ │ Alignment handling │ │
│ │ realloc / grow / shrink │ │
│ │ Mutex │ │
│ └───────────┬────────────────────┘ │
│ │ depends on only 3 APIs│
│ ▼ │
│ ┌────────────────────────────────┐ │
│ │ FreeBlockManager │ │
│ │ • take_out(size) → block │ │
│ │ • register(block) │ │
│ │ • unregister(block) │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────┘
LeanFlexAllocatorhandles the logic common to all allocation strategies: take a free block fromFreeBlockManager→ split as needed → return to caller; on deallocation, coalesce adjacent free blocks → register back. This logic is identical regardless of the search strategy.FreeBlockManageronly cares about how free blocks are organized: TLSF's two-level bitmap? HalfTree's binary tree? Or a simple FirstFit linked list? Just implement the three core operations:take_out/register/unregister.
This separation yields two key benefits:
- Adding a new strategy is trivial: implement just three APIs — no need to rewrite splitting/coalescing code.
- Common logic is written once: splitting, coalescing, alignment handling, realloc optimization, etc. are maintained in a single place.
Analogy:
FreeBlockManageris to the allocator what a hash function is toHashMap— swap the strategy by swapping one component.
Allocation Strategies
| Type | Description | Node Size |
|---|---|---|
Tlsf |
Two-Level Segregated Fit — O(1) allocation, best general-purpose choice | 2 pointers |
HalfTree |
Binary tree-based half-tree algorithm, pairs well with DesignatedVictim |
3 pointers |
FirstFit |
Simple first-fit linked list search, lowest overhead | 1 pointer |
SimpleFirstFit |
Minimal implementation, suitable for testing and controlled environments | 1 pointer |
DesignatedVictim Wrapper
DesignatedVictim<MIN_SIZE, MAX_SIZE, Manager> is a generic FreeBlockManager wrapper. It maintains a "designated victim" (caching the most recently freed small block) on top of the inner manager, similar to dlmalloc's DV mechanism. Small frees are preferentially cached rather than immediately returned to the inner manager; subsequent allocations of the same size hit in O(1), reducing fragmentation and improving locality. Recommended pairing with HalfTree: DesignatedVictim<64, 256, HalfTree<...>>.
Configurable Parameters
ealloc pursues zero-overhead at compile time — all parameters are determined via generics with no virtual dispatch at runtime. There are three core parameters: StateElement, Element, and OptiPtr. They are interrelated and jointly determine the allocator's memory overhead, management range, and fragmentation characteristics.
Parameter Interaction Overview
The table below shows the effects of different parameter combinations using typical embedded scenarios (assuming TLSF with 2 pointers per node):
| Scenario | StateElement | Element | OptiPtr | Node Size | Max Block | Address Range | Suitable Memory |
|---|---|---|---|---|---|---|---|
| Tiny MCU | u8 |
u16 |
NonZero<u16> |
4 B | 254 B | 128 KiB | ≤ 64 KB SRAM |
| Small MCU | u8 |
u32 |
NonZero<u16> |
4 B | 508 B | 256 KiB | ≤ 256 KB SRAM |
| Medium MCU | u16 |
u32 |
NonZero<u16> |
4 B | ~128 KiB | 256 KiB | ≤ 512 KB SRAM |
| Medium MCU | u16 |
u64 |
NonZero<u16> |
4 B | ~256 KiB | 512 KiB | ≤ 1 MB SRAM |
| Large MCU | usize |
u64 |
NonZero<u16> |
4 B | platform limit | 512 KiB | multi-MB SRAM |
| General | usize |
u64 |
NonNull<u64> |
16 B | platform limit | full space | any |
Key relationships: Max block = f(StateElement, Element); Node overhead = f(OptiPtr); Address range = f(OptiPtr, Element). In embedded scenarios,
NonZero<u16>is almost always the best choice — only 2 bytes per pointer, with an address range sufficient for most MCU SRAM.
StateElement — Controls Max Block Size
Determines the bit-width of the block size counter. Must satisfy size_of::<Self>() <= size_of::<usize>(). Block size is counted in Element units; the bit-width limits the maximum number of elements in a single block. For embedded use, typically choose u8 or u16:
| StateElement | Max Elements | Max Block (Element=u32) | Max Block (Element=u64) | Typical Scenario |
|---|---|---|---|---|
u8 |
127 | 508 B | ~1 KiB | Tiny MCU (≤ 64 KB) |
u16 |
32,767 | ~128 KiB | ~256 KiB | Small/medium MCU (≤ 1 MB) |
usize |
— | isize::MAX |
isize::MAX |
Unlimited / general-purpose |
Max elements =
StateElement::MAX >> 1(one bit reserved for free/used flag). Whensize_of::<StateElement>() == size_of::<usize>(), the count stores bytes directly instead of elements, so max block isisize::MAX. SmallerStateElementsaves per-block header storage (2 × StateElement per block).
Element — Alignment Granularity and Internal Fragmentation
Memory is organized in units of Element. All allocation sizes and addresses are multiples of size_of::<Element>(). Element can be any type (not limited to primitive integers), e.g. u32, u64, [u32; 2], as long as size % align == 0 and align >= 2.
Every allocated block carries a fixed overhead: a Joint (2 × StateElement) at each boundary for coalescing, plus the free-block Node at the head when the block is free. A free block must be large enough to hold both the Node and the tail Joint, setting the minimum block size = Node + Joint::SIZE (rounded up to Element alignment).
The table below uses TLSF with NonZero<u16> + u16 StateElement as a concrete example (Node = 4 B, Joint = 4 B):
| Element | Granularity | Min Free Block | Block for 18 B alloc | Usable | Frag | Addr Range (NonZero<u16>) |
|---|---|---|---|---|---|---|
u16 |
2 B | 4u × 2 = 8 B | 11u × 2 = 22 B | 18 B | 0 B | 128 KiB |
u32 |
4 B | 2u × 4 = 8 B | 6u × 4 = 24 B | 20 B | 2 B | 256 KiB |
u64 |
8 B | 1u × 8 = 8 B | 3u × 8 = 24 B | 20 B | 2 B | 512 KiB |
Block size =
ceil((user_size + Joint::SIZE), align), Usable = block − Joint::SIZE, Frag = Usable − user_size. Fragmentation only occurs when rounding pushes the block beyond what the user requested plus overhead.
- Smaller Element: finer granularity, lower fragmentation floor, but smaller manageable memory range for a given
StateElement, and smallerOptiPtraddress range. - Larger Element: larger management and address ranges, but higher minimum allocation size and more internal fragmentation for small allocations.
Over-aligned allocations: when the requested alignment exceeds align_of::<Element>(), the allocator falls back to a slower over-aligned path that reserves extra elements at the block front for pointer realignment. This path is fully supported but carries a performance cost — choose Element alignment to match your most common allocation alignment, not necessarily max_align_t. The trade-off is: larger Element (higher alignment) = fewer over-aligned fallbacks, but more fragmentation for small objects.
OptiPtr — Narrowed Pointers
Free block nodes store pointers to other free blocks. In embedded scenarios, using full-width pointers (4–8 bytes) for these links is extremely wasteful. ealloc supports narrowed pointers via the OptimizedPtr trait:
| Pointer Type | Width (32-bit) | Width (64-bit) | Address Range | Use Case |
|---|---|---|---|---|
NonNull<Element> |
4 B | 8 B | Full address space | General / heap |
NonZero<u16> |
2 B | 2 B | 64K Elements | Embedded MCU |
NonZero<u32> |
— | 4 B | 4G Elements | 64-bit large memory |
FixedBasePtr<NonZero<u16>> |
2 B | 2 B | 64K Elements (fixed base) | Static memory pool |
Taking TLSF as an example (node = 2 pointers + block state), per-node cost for each pointer width:
| OptiPtr | Node Size | 100 Free Blocks Total Overhead |
|---|---|---|
NonNull<u64> |
20 B | 2000 B |
NonNull<u32> |
12 B | 1200 B |
NonZero<u16> |
6 B | 600 B |
In MCU scenarios, the number of free blocks can reach dozens to hundreds; the memory saved by narrowed pointers is significant. NonZero<u16> covers virtually all MCU SRAM sizes and is the most common choice.
TLSF-Specific Parameters
FlMap/SlMap: Integer types for first-level and second-level bitmaps. TypicallyFlMapusesu16/u32,SlMapusesu8.FL/SL: Number of first-level and second-level bins.SLmust be a power of two. UseTlsfParms::calc_two_level_for_size(max_region_size)for automatic calculation.
HalfTree-Specific Parameters
BINS_COUNT: Number of bins, which determines the maximum block size that can be managed. Each bin corresponds to a size class (power of two). Must be less thanStateElement::BITS.
Quick Example
use ;
use NonZero;
// StateElement=u16 → max single block ~256 KB (Element=u64)
// Element=u64 → 8-byte alignment granularity
// OptiPtr=u16 → node pointers only 2 bytes, saving memory
// FlMap=u16 → supports up to 16 first-level bits
// FL=13, SL=8 → 13×8 two-level matrix
type Alloc = ;
static mut BUF: = ; // 32 KiB
let alc = new;
// unsafe { alc.init_with_slice(&mut BUF).unwrap(); }
Features
| Feature | Default | Description |
|---|---|---|
std |
Enable std::sync::Mutex as AllocatorMutex |
MSRV
Rust 1.87+
License
MIT — see repository for full license.