initial noalloc implementation

This commit is contained in:
husky 2025-06-30 17:44:35 -07:00
parent d0ae2d59ad
commit 3e2973ad3b
7 changed files with 274 additions and 1 deletions

7
.gitignore vendored
View file

@ -18,4 +18,9 @@ target
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
# Added by cargo
/target

7
Cargo.lock generated Normal file
View file

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "atomalloc"
version = "0.1.0"

11
Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "atomalloc"
version = "0.1.0"
edition = "2024"
[dependencies]
[features]
default = ["no_alloc", "with_alloc"]
no_alloc = []
with_alloc = []

9
src/lib.rs Normal file
View file

@ -0,0 +1,9 @@
#![no_std]
#[cfg(feature = "no_alloc")]
pub mod noalloc;
#[cfg(feature = "with_alloc")]
pub mod withalloc;
#[cfg(test)]
mod tests;

209
src/noalloc.rs Normal file
View file

@ -0,0 +1,209 @@
pub struct AtomAlloc<const SLOTS: usize, const FRAME_SIZE: usize> {
pub atoms: [Option<Atom<FRAME_SIZE>>; SLOTS],
pub head: Option<usize>,
}
#[derive(Eq, PartialEq, Debug)]
pub struct Atom<const FRAME_SIZE: usize> {
pub start: usize,
pub frame_count: usize,
}
impl<const SLOTS: usize, const FRAME_SIZE: usize> Default for AtomAlloc<SLOTS, FRAME_SIZE> {
fn default() -> Self {
Self::new()
}
}
impl<const SLOTS: usize, const FRAME_SIZE: usize> AtomAlloc<SLOTS, FRAME_SIZE> {
pub const fn new() -> Self {
Self {
atoms: [const { None }; SLOTS],
head: None,
}
}
fn head(&self) -> &Option<Atom<FRAME_SIZE>> {
// self.head should always point to either a valid atom or a none
if let Some(index) = self.head {
&self.atoms[index]
} else {
&None
}
}
fn push(&mut self, atom: Atom<FRAME_SIZE>) {
if atom.frame_count == 0 {
// empty atom, do nothing
return;
}
if let Some(index) = self.head {
// if there's an atom at the head, add one and put an atom there
self.atoms[index + 1] = Some(atom);
self.head = Some(index + 1);
} else {
// no atom, add the first atom
self.head = Some(0);
self.atoms[0] = Some(atom);
}
}
fn pop(&mut self) -> Option<Atom<FRAME_SIZE>> {
if let Some(index) = self.head {
// if there's an atom at the head...
if index == 0 {
// if the atom is the first one, unset head and return first atom
self.head = None;
self.atoms[0].take()
} else {
// take and subtract head
self.head = Some(index - 1);
self.atoms[index - 1].take()
}
} else {
None
}
}
/// # add_memory
/// this function will add memory to the allocator, starting at `start`
/// `frame_count` is the amount of frames that are available after `start`
///
/// no alignment guarantees are made,
/// so if you want alignment then do that before inserting the frames
pub fn add_memory(&mut self, start: usize, frame_count: usize) {
self.push(Atom { start, frame_count })
}
/// # allocate
/// this function will find a free space of the given `frame_count`;
/// the starting address will be returned, once again with no alignment guarantees
/// will return `None` if there is not enough memory to allocate the given `frame_count`
/// if there is enough time, it may be beneficial to call `defragment()` beforehand
pub fn allocate(&mut self, frame_count: usize) -> Option<usize> {
// is the head atom good enough?
let top = self.pop()?;
if top.frame_count >= frame_count {
// allocate from the start
self.push(Atom {
start: top.start + (frame_count * FRAME_SIZE),
frame_count: top.frame_count - frame_count,
});
Some(top.start)
} else {
// search all of the slots
let mut search_stack = [const { None }; SLOTS];
search_stack[0] = Some(top);
let mut head = 0;
let mut found = None;
while found.is_none() {
let top = self.pop()?;
if top.frame_count >= frame_count {
found = Some(top);
} else {
head += 1;
search_stack[head] = Some(top);
}
}
// refill
while head != 0 {
let top = search_stack[head].take()?;
head -= 1;
self.push(top);
}
if let Some(found) = found {
// this is the atom we can allocate from
self.push(Atom {
start: found.start + (frame_count * FRAME_SIZE),
frame_count: found.frame_count - frame_count,
});
Some(found.start)
} else {
None
}
}
}
/// # deallocate
/// this function is a mirror of `add_memory()`, you must provide the amount of frames
/// allocated, it won't work with a raw memory length
pub fn deallocate(&mut self, start: usize, frame_count: usize) {
self.add_memory(start, frame_count);
}
/// # defragment
/// this function will rearrange the atoms available in the allocator so that they
/// follow each other, and merge touching atoms
///
/// this function does not check for overlapping atoms
pub fn defragment(&mut self) {
if let Some(head) = self.head {
if head == 0 {
return;
}
// first we will sort the atoms, i'm just gonna use insertion sort cause its simple
let mut idx = 1;
while idx <= head {
let atom = self.atoms[idx].take().expect("None before head!");
// go back through and insert the atom there
let mut idx2 = idx;
while idx2 > 0 {
idx2 -= 1;
// fixme: this can be optimized using std::mem::swap
// take the previous atom
let prev_atom = self.atoms[idx2].take().expect("None before head!");
// if the previous atom is greater, place current before it
if prev_atom.start > atom.start {
self.atoms[idx2 + 1] = Some(prev_atom);
} else {
// place atom here
self.atoms[idx2 + 1] = Some(prev_atom);
self.atoms[idx2] = Some(atom);
break;
}
if idx2 == 0 {
self.atoms[idx2] = Some(atom);
break;
}
}
idx += 1;
}
// should be sorted now, merge touching atoms
let mut stack = [const { None }; SLOTS];
// clippy, shut up
#[allow(clippy::needless_range_loop)]
for i in 0..=head {
stack[head - i] = self.atoms[i].take()
}
let mut idx = head;
let mut idx2 = 0;
let mut atom = stack[head].take().expect("None before head!");
loop {
// take next atom
if idx == 0 {
self.atoms[idx2] = Some(atom);
break;
}
let next = stack[idx - 1].take().expect("None before head!");
if atom.start + (atom.frame_count * FRAME_SIZE) >= next.start {
// merge
atom.frame_count += next.frame_count;
} else {
// cannot merge anymore, push and take
self.atoms[idx2] = Some(atom);
idx2 += 1;
atom = next;
}
idx -= 1;
}
self.head = Some(idx2);
}
}
}

32
src/tests.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::noalloc::{Atom, AtomAlloc};
#[test]
fn noalloc() {
let mut alloc: AtomAlloc<16, 1> = AtomAlloc::default();
assert_eq!(alloc.head, None);
alloc.add_memory(1024, 64); // [(1024, 64)...]
assert_eq!(alloc.atoms[0], Some(Atom { start: 1024, frame_count: 64 }));
assert_eq!(alloc.head, Some(0));
// allocation
let start = alloc.allocate(10); // [(1034, 54)...]
assert_eq!(alloc.atoms[0], Some(Atom { start: 1034, frame_count: 54 }));
assert_eq!(start, Some(1024));
assert_eq!(alloc.head, Some(0));
// deallocation
alloc.deallocate(1024, 10); // [(1034, 54), (1024, 10)...]
assert_eq!(alloc.atoms[0], Some(Atom { start: 1034, frame_count: 54 }));
assert_eq!(alloc.atoms[1], Some(Atom { start: 1024, frame_count: 10 }));
assert_eq!(alloc.head, Some(1));
// defragmentation
alloc.defragment(); // [(1024, 64)...]
assert_eq!(alloc.atoms[0], Some(Atom { start: 1024, frame_count: 64 }));
assert_eq!(alloc.head, Some(0));
}

0
src/withalloc.rs Normal file
View file