Crates.io | tlsf |
lib.rs | tlsf |
version | 1.1.0 |
source | src |
created_at | 2023-12-05 18:04:45.602077 |
updated_at | 2024-01-12 18:09:45.158794 |
description | An implementation of the Two-Level Segregated Fit (TLSF) allocator with optimized memory footprint |
homepage | |
repository | https://github.com/japaric/tlsf |
max_upload_size | |
id | 1058911 |
size | 291,708 |
An implementation of the Two-Level Segregated Fit (TLSF) allocator with optimized memory footprint
alloc
and dealloc
1 operations execute in bounded constant time (O(1)
)For more details check the papers linked in the 'References' section
usize
-> u16
)miri
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.
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.
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
FLL |
FL | MAX_ALLOC_SIZE |
HEADER_SIZE |
---|---|---|---|
1 | 6 | 60 B | 36 B |
2 | 7 | 124 B | 72 B |
3 | 8 | 248 B | 104 B |
4 | 9 | 496 B | 140 B |
5 | 10 | 992 B | 172 B |
6 | 11 | 1,984 B | 208 B |
7 | 12 | 3,968 B | 240 B |
8 | 13 | 7,936 B | 276 B |
9 | 14 | 15,872 B | 308 B |
10 | 15 | 31,744 B | 344 B |
11 | 16 | 63,488 B | 376 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.
Benchmark configuration
thumbv7em-none-eabi
~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.
workloads:
memalign
: N random-sized (< MAX_ALLOC_SIZE
) allocations with random alignments (<= 32 B) until
OOMOperation | min | max |
---|---|---|
memalign (ALL) |
66 | 407 |
memalign (FAIL) |
66 | 125 |
memalign (OK) |
193 | 407 |
free |
96 | 337 |
"FAIL" indicates that memalign
returned None
; "OK" indicates that it returned Some
; "ALL"
accounts for both cases.
![][memalign-histograms]
malloc
: N random-sized (< MAX_ALLOC_SIZE
) allocations until OOM followed by N deallocations in
reverse orderOperation | min | max |
---|---|---|
malloc (ALL) |
91 | 292 |
malloc (FAIL) |
91 | 103 |
malloc (OK) |
173 | 292 |
free |
98 | 237 |
![][malloc-histograms]
The code used to make the measurements can be found in the thumbv7em
directory of the project's
repository.
The design of the TLSF allocator is described and discussed in the following papers
which are available at http://www.gii.upv.es/tlsf/main/docs.html
in the worst case scenario, the realloc
operation involves a memcpy
operation, which executes in linear time (O(N)
) ↩