Crate tlsf

source ·
Expand description

An implementation of the Two-Level Segregated Fit (TLSF) allocator with optimized memory footprint

Design features

  • The alloc and dealloc 1 operations execute in bounded constant time (O(1))
  • Good-fit strategy
  • Immediate coalescing adds predictability and reduces fragmentation

For more details check the papers linked in the ‘References’ section

Implementation features

  • Reduced memory footprint compared to the original TLSF thanks to pointer compression (usize -> u16)
  • Free of panicking branches when optimized, even when debug-assertions are enabled
  • Adheres to strict provenance as checked by miri

Rejected features

  • A GlobalAlloc implementation.

Rationale: it can be implemented on top of this library and every implementation needs to make app-specific decisions like synchronization and whether to “forbid” realloc-like operations which don’t have bounded execution time by always triggering an OOM for them.

Those decisions are best left to the application author.

Limitations

  • Can manage only up to 256 KiB of contiguous memory
  • Can only allocate sizes of up to 62 KiB

Examples

The allocator manages mutably borrowed memory; the memory can even be stack allocated.

use core::alloc::Layout;
use core::mem::MaybeUninit;

use tlsf::Tlsf;

let mut tlsf = Tlsf::<1>::empty();
let mut memory = [MaybeUninit::uninit(); 256];
tlsf.initialize(&mut memory);

let alloc: &mut [MaybeUninit<u32>] = tlsf.memalign(Layout::new::<u32>()).unwrap();
assert!(alloc.len() >= 1);
alloc.iter_mut().for_each(|mu| { mu.write(42); });

When alignment is not important, the malloc method can be used. The returned block will have a minimal alignment of 4 bytes.

use core::alloc::Layout;
use core::mem::MaybeUninit;
use core::num::NonZeroU16;

use tlsf::Tlsf;

let mut tlsf = Tlsf::<1>::empty();
let mut memory = [MaybeUninit::uninit(); 256];
tlsf.initialize(&mut memory);

let size = NonZeroU16::new(4).unwrap();
let alloc: &mut [MaybeUninit<u32>] = tlsf.malloc(size).unwrap();
assert!(alloc.len() >= 1);
alloc.iter_mut().for_each(|mu| { mu.write(42); });

The allocator tracks the lifetime of the initial memory pool and allocations cannot outlive it. This code does not compile

use core::alloc::Layout;
use core::mem::MaybeUninit;

use tlsf::Tlsf;

let mut tlsf = Tlsf::<1>::empty();

{
   let memory = [MaybeUninit::uninit(); 256];
   tlsf.initialize(&mut memory); //~ error: `memory` does not live long enough
   // `memory` goes out of scope here
}

let alloc = tlsf.memalign(Layout::new::<u64>());

Due to this lifetime constraint, usage with #[global_allocator] requires that the initial memory pool has 'static lifetime. An example GlobalAlloc implementation can be found in the thumbv7em directory of this project’s repository.

Parameters

The TLSF allocator has 2 parameters: FL and SL (see linked paper for further details). This implementation hard codes SL to 16. FL can controlled via the FLL type parameter of the Tlsf type. The table below shows the possible values of FLL and its effect on the allocator

FLLFLMAX_ALLOC_SIZEHEADER_SIZE
1660 B36 B
27124 B72 B
38248 B104 B
49496 B140 B
510992 B172 B
6111,984 B208 B
7123,968 B240 B
8137,936 B276 B
91415,872 B308 B
101531,744 B344 B
111663,488 B376 B

Requesting more than MAX_ALLOC_SIZE bytes of memory from the allocator will always result in an OOM condition. Note that the effective value of MAX_ALLOC_SIZE is reduced when alignments greater than 4 are requested via memalign due to potential padding needed to meet the alignment requirement.

HEADER_SIZE is the fixed memory overhead of the allocator. There’s a 4 or 8 byte of overhead for each memory block managed by the allocator.

Performance

Benchmark configuration

  • rustc: 1.74.0
  • target: thumbv7em-none-eabi
  • profile: release (opt-level=3, lto=‘fat’, codegen-units=1)
  • FLL: 2

Binary (“Flash”) size

~1,594 B (.text section) if only memalign is used

~1,440 B (.text section) if only malloc is used

Measured using

#![no_main]
#![no_std]

#[no_mangle]
fn _start() -> [usize; 3] {
    [
        Tlsf::<2>::free as usize,
        Tlsf::<2>::initialize as usize,
        Tlsf::<2>::memalign as usize, // OR malloc
    ]
}
$ size -A binary
section           size     addr
.ARM.exidx           16    65748
.text              1594   131300
.debug_aranges       0        0

Note that these measurements used optimization for speed and are meant to give you an idea of the binary footprint of the library. Applying optimizations for size will obviously result in a smaller footprint.

Execution time

workloads:

  • memalign: N random-sized (< MAX_ALLOC_SIZE) allocations with random alignments (<= 32 B) until OOM
Operationminmax
memalign (ALL)66407
memalign (FAIL)66125
memalign (OK)193407
free96337

“FAIL” indicates that memalign returned None; “OK” indicates that it returned Some; “ALL” accounts for both cases.

  • malloc: N random-sized (< MAX_ALLOC_SIZE) allocations until OOM followed by N deallocations in reverse order
Operationminmax
malloc (ALL)91292
malloc (FAIL)91103
malloc (OK)173292
free98237

The code used to make the measurements can be found in the thumbv7em directory of the project’s repository.

References

The design of the TLSF allocator is described and discussed in the following papers

  • “A constant-time dynamic storage allocator for real-time systems.”
  • “Implementation of a constant time dynamic storage allocator.”
  • “TLSF: A new dynamic memory allocator for real-time systems.”

which are available at http://www.gii.upv.es/tlsf/main/docs.html


  1. in the worst case scenario, the realloc operation involves a memcpy operation, which executes in linear time (O(N)

Structs

  • A memory block managed by a Tlsf allocator
  • The Two-Level Segregated Fit (TLSF) memory allocator