use alloc::collections::{BTreeMap, BTreeSet, btree_map::Entry};
use core::{alloc::Layout, num::NonZeroU32};
pub(crate) struct FreeList {
capacity: usize,
free_block_index_to_len: BTreeMap<u32, u32>,
free_block_len_to_indices: BTreeMap<u32, BTreeSet<u32>>,
bump_ptr: u32,
bump_end: u32,
}
const ALIGN_U32: u32 = 16;
const ALIGN_USIZE: usize = ALIGN_U32 as usize;
impl FreeList {
pub fn layout(size: usize) -> Layout {
Layout::from_size_align(size, ALIGN_USIZE).unwrap()
}
#[inline]
pub fn aligned_size(size: u32) -> Option<u32> {
Some((size.checked_add(ALIGN_U32)? - 1) & !(ALIGN_U32 - 1))
}
pub fn current_capacity(&self) -> usize {
self.capacity
}
pub fn new(capacity: usize) -> Self {
log::debug!("FreeList::new({capacity})");
let start = ALIGN_U32;
let mut free_list = FreeList {
capacity,
free_block_index_to_len: BTreeMap::new(),
free_block_len_to_indices: BTreeMap::new(),
bump_ptr: start,
bump_end: start,
};
let end = u32::try_from(free_list.capacity).unwrap_or_else(|_| {
assert!(free_list.capacity > usize::try_from(u32::MAX).unwrap());
u32::MAX
});
let len = round_u32_down_to_pow2(end.saturating_sub(start), ALIGN_U32);
if len >= ALIGN_U32 {
free_list.bump_end = start + len;
}
free_list.check_integrity();
free_list
}
pub fn add_capacity(&mut self, additional: usize) {
let old_cap = self.capacity;
self.capacity = self.capacity.saturating_add(additional);
log::debug!(
"FreeList::add_capacity({additional:#x}): capacity growing from {old_cap:#x} to {:#x}",
self.capacity
);
let old_cap_rounded = round_usize_down_to_pow2(old_cap, ALIGN_USIZE);
let Ok(old_cap_rounded) = u32::try_from(old_cap_rounded) else {
return;
};
let index = NonZeroU32::new(old_cap_rounded).unwrap_or(
NonZeroU32::new(ALIGN_U32).unwrap(),
);
let new_cap = u32::try_from(self.capacity).unwrap_or(u32::MAX);
let new_cap = round_u32_down_to_pow2(new_cap, ALIGN_U32);
if index.get() > new_cap {
return;
}
let size = new_cap - index.get();
debug_assert_eq!(size % ALIGN_U32, 0);
if size == 0 {
return;
}
log::trace!(
"FreeList::add_capacity(..): adding block {index:#x}..{:#x}",
index.get() + size
);
self.dealloc_impl(index.get(), size);
}
#[cfg(test)]
fn max_size(&self) -> usize {
let cap = core::cmp::min(self.capacity, usize::try_from(u32::MAX).unwrap());
round_usize_down_to_pow2(cap.saturating_sub(ALIGN_USIZE), ALIGN_USIZE)
}
#[cfg(test)]
fn num_free_blocks(&self) -> usize {
self.free_block_index_to_len.len() + if self.bump_end > self.bump_ptr { 1 } else { 0 }
}
pub fn can_align_to(align: usize) -> bool {
debug_assert!(align.is_power_of_two());
align <= ALIGN_USIZE
}
#[cfg(test)]
fn check_layout(&self, layout: Layout) -> crate::Result<u32> {
crate::ensure!(
Self::can_align_to(layout.align()),
"requested allocation's alignment of {} is greater than max supported \
alignment of {ALIGN_USIZE}",
layout.align(),
);
let alloc_size = u32::try_from(layout.size()).map_err(|e| {
let trap = crate::Trap::AllocationTooLarge;
let err = crate::Error::from(trap);
err.context(e)
.context("requested allocation's size does not fit in a u32")
})?;
alloc_size
.checked_next_multiple_of(ALIGN_U32)
.ok_or_else(|| {
let trap = crate::Trap::AllocationTooLarge;
let err = crate::Error::from(trap);
let err = err.context(format!(
"failed to round allocation size of {alloc_size} up to next \
multiple of {ALIGN_USIZE}"
));
err
})
}
#[cfg(test)]
pub fn alloc(&mut self, layout: Layout) -> crate::Result<Option<NonZeroU32>> {
log::trace!("FreeList::alloc({layout:?})");
let alloc_size = self.check_layout(layout)?;
Ok(self.alloc_impl(alloc_size))
}
#[inline]
pub fn alloc_fast(&mut self, alloc_size: u32) -> Option<NonZeroU32> {
debug_assert_eq!(alloc_size % ALIGN_U32, 0);
debug_assert!(alloc_size > 0);
self.alloc_impl(alloc_size)
}
#[inline]
fn alloc_impl(&mut self, alloc_size: u32) -> Option<NonZeroU32> {
debug_assert_eq!(
Self::layout(usize::try_from(alloc_size).unwrap()).size(),
usize::try_from(alloc_size).unwrap()
);
debug_assert_eq!(alloc_size % ALIGN_U32, 0);
if let Some(new_ptr) = self.bump_ptr.checked_add(alloc_size)
&& new_ptr <= self.bump_end
{
let result = self.bump_ptr;
self.bump_ptr = new_ptr;
debug_assert_ne!(result, 0);
debug_assert_eq!(result % ALIGN_U32, 0);
self.check_integrity();
log::trace!("FreeList::alloc -> {result:#x}");
return Some(unsafe { NonZeroU32::new_unchecked(result) });
}
self.check_integrity();
self.alloc_slow(alloc_size)
}
#[cold]
fn alloc_slow(&mut self, alloc_size: u32) -> Option<NonZeroU32> {
let remaining_ptr = self.bump_ptr;
let remaining = self.bump_end - self.bump_ptr;
self.bump_ptr = ALIGN_U32;
self.bump_end = ALIGN_U32;
if remaining >= ALIGN_U32 {
self.insert_free_block(remaining_ptr, remaining);
}
let (block_index, block_len) = self
.free_block_len_to_indices
.range_mut(alloc_size..)
.find_map(|(&block_len, indices)| {
debug_assert!(block_len >= alloc_size);
debug_assert_eq!(block_len % ALIGN_U32, 0);
let block_index = indices.pop_first()?;
Some((block_index, block_len))
})?;
let Entry::Occupied(entry) = self.free_block_len_to_indices.entry(block_len) else {
unreachable!()
};
if entry.get().is_empty() {
entry.remove();
}
let block_len2 = self.free_block_index_to_len.remove(&block_index);
debug_assert_eq!(block_len, block_len2.unwrap());
debug_assert_eq!(block_index % ALIGN_U32, 0);
debug_assert_eq!(block_len % ALIGN_U32, 0);
debug_assert_ne!(block_len, 0);
debug_assert!(block_len >= alloc_size);
self.bump_ptr = block_index + alloc_size;
self.bump_end = block_index + block_len;
debug_assert_ne!(block_index, 0);
self.check_integrity();
Some(unsafe { NonZeroU32::new_unchecked(block_index) })
}
#[cfg(test)]
pub fn dealloc(&mut self, index: NonZeroU32, layout: Layout) {
let alloc_size = self.check_layout(layout).unwrap();
self.dealloc_impl(index.get(), alloc_size);
}
#[inline]
pub fn dealloc_fast(&mut self, index: NonZeroU32, alloc_size: u32) {
debug_assert_eq!(alloc_size % ALIGN_U32, 0);
debug_assert_eq!(index.get() % ALIGN_U32, 0);
self.dealloc_impl(index.get(), alloc_size);
}
#[inline]
fn dealloc_impl(&mut self, index: u32, alloc_size: u32) {
log::trace!("FreeList::dealloc({index:#x}, {alloc_size:?})");
debug_assert_eq!(index % ALIGN_U32, 0);
debug_assert_eq!(alloc_size % ALIGN_U32, 0);
if index + alloc_size == self.bump_ptr {
self.bump_ptr = index;
if let Some((&block_index, &block_len)) = self.free_block_index_to_len.last_key_value()
{
if block_index + block_len == self.bump_ptr {
self.bump_ptr = block_index;
let last = self.free_block_index_to_len.pop_last();
debug_assert_eq!((block_index, block_len), last.unwrap());
self.remove_from_block_len_to_index(block_index, block_len);
}
}
self.check_integrity();
return;
}
if self.bump_end == index {
self.bump_end = index + alloc_size;
if let Some(block_len) = self.free_block_index_to_len.remove(&self.bump_end) {
self.remove_from_block_len_to_index(self.bump_end, block_len);
self.bump_end += block_len;
}
self.check_integrity();
return;
}
let prev_block = self
.free_block_index_to_len
.range(..index)
.next_back()
.map(|(idx, len)| (*idx, *len));
let next_block = self
.free_block_index_to_len
.range(index + 1..)
.next()
.map(|(idx, len)| (*idx, *len));
match (prev_block, next_block) {
(Some((prev_index, prev_len)), Some((next_index, next_len)))
if blocks_are_contiguous(prev_index, prev_len, index)
&& blocks_are_contiguous(index, alloc_size, next_index) =>
{
log::trace!(
"merging blocks {prev_index:#x}..{prev_end:#x}, {index:#x}..{index_end:#x}, {next_index:#x}..{next_end:#x}",
prev_end = prev_index + prev_len,
index_end = index + alloc_size,
next_end = next_index + next_len,
);
let next_len2 = self.free_block_index_to_len.remove(&next_index);
debug_assert_eq!(next_len, next_len2.unwrap());
self.remove_from_block_len_to_index(next_index, next_len);
let merged_block_len = next_index + next_len - prev_index;
debug_assert_eq!(merged_block_len % ALIGN_U32, 0);
*self.free_block_index_to_len.get_mut(&prev_index).unwrap() = merged_block_len;
self.remove_from_block_len_to_index(prev_index, prev_len);
self.free_block_len_to_indices
.entry(merged_block_len)
.or_default()
.insert(prev_index);
}
(Some((prev_index, prev_len)), _)
if blocks_are_contiguous(prev_index, prev_len, index) =>
{
log::trace!(
"merging blocks {prev_index:#x}..{prev_end:#x}, {index:#x}..{index_end:#x}",
prev_end = prev_index + prev_len,
index_end = index + alloc_size,
);
let merged_block_len = index + alloc_size - prev_index;
debug_assert_eq!(merged_block_len % ALIGN_U32, 0);
*self.free_block_index_to_len.get_mut(&prev_index).unwrap() = merged_block_len;
self.remove_from_block_len_to_index(prev_index, prev_len);
self.free_block_len_to_indices
.entry(merged_block_len)
.or_default()
.insert(prev_index);
}
(_, Some((next_index, next_len)))
if blocks_are_contiguous(index, alloc_size, next_index) =>
{
log::trace!(
"merging blocks {index:#x}..{index_end:#x}, {next_index:#x}..{next_end:#x}",
index_end = index + alloc_size,
next_end = next_index + next_len,
);
let next_len2 = self.free_block_index_to_len.remove(&next_index);
debug_assert_eq!(next_len, next_len2.unwrap());
self.remove_from_block_len_to_index(next_index, next_len);
let merged_block_len = next_index + next_len - index;
debug_assert_eq!(merged_block_len % ALIGN_U32, 0);
self.free_block_index_to_len.insert(index, merged_block_len);
self.free_block_len_to_indices
.entry(merged_block_len)
.or_default()
.insert(index);
}
(_, _) => {
log::trace!("cannot merge blocks");
self.free_block_index_to_len.insert(index, alloc_size);
self.free_block_len_to_indices
.entry(alloc_size)
.or_default()
.insert(index);
}
}
if let Some((&block_index, &block_len)) = self.free_block_index_to_len.last_key_value() {
if block_index + block_len == self.bump_ptr {
self.bump_ptr = block_index;
let last = self.free_block_index_to_len.pop_last();
debug_assert_eq!((block_index, block_len), last.unwrap());
self.remove_from_block_len_to_index(block_index, block_len);
}
}
self.check_integrity();
}
#[track_caller]
fn remove_from_block_len_to_index(&mut self, block_index: u32, block_len: u32) {
let Entry::Occupied(mut entry) = self.free_block_len_to_indices.entry(block_len) else {
unreachable!()
};
let was_present = entry.get_mut().remove(&block_index);
debug_assert!(was_present);
if entry.get().is_empty() {
entry.remove();
}
}
pub fn iter_free_blocks(&self) -> impl Iterator<Item = (u32, u32)> + '_ {
let bump = if self.bump_end > self.bump_ptr {
Some((self.bump_ptr, self.bump_end - self.bump_ptr))
} else {
None
};
self.free_block_index_to_len
.iter()
.map(|(idx, len)| (*idx, *len))
.chain(bump)
}
fn insert_free_block(&mut self, index: u32, size: u32) {
debug_assert_eq!(index % ALIGN_U32, 0);
debug_assert_eq!(size % ALIGN_U32, 0);
self.dealloc_impl(index, size);
}
fn check_integrity(&self) {
if !cfg!(gc_zeal) {
return;
}
let mut prev_end = None;
for (&index, &len) in self.free_block_index_to_len.iter() {
let end = index + len;
assert!(usize::try_from(end).unwrap() <= self.capacity);
if let Some(prev_end) = prev_end {
assert!(prev_end < index);
}
assert_eq!(index % ALIGN_U32, 0);
assert_eq!(len % ALIGN_U32, 0);
assert!(self.free_block_len_to_indices.contains_key(&len));
assert!(self.free_block_len_to_indices[&len].contains(&index));
prev_end = Some(end);
}
for (len, indices) in &self.free_block_len_to_indices {
assert!(!indices.is_empty());
for idx in indices {
assert!(self.free_block_index_to_len.contains_key(idx));
assert_eq!(self.free_block_index_to_len[idx], *len);
}
}
assert!(self.bump_ptr <= self.bump_end);
assert_ne!(self.bump_ptr, 0);
assert_ne!(self.bump_end, 0);
if self.bump_ptr < self.bump_end {
assert_eq!(self.bump_ptr % ALIGN_U32, 0);
assert_eq!(self.bump_end % ALIGN_U32, 0);
assert!(usize::try_from(self.bump_end).unwrap() <= self.capacity);
for (&index, &len) in self.free_block_index_to_len.iter() {
let block_end = index + len;
assert!(
self.bump_end <= index || self.bump_ptr >= block_end,
"bump region [{}, {}) overlaps with block [{}, {})",
self.bump_ptr,
self.bump_end,
index,
block_end
);
}
}
}
}
#[inline]
fn blocks_are_contiguous(prev_index: u32, prev_len: u32, next_index: u32) -> bool {
let end_of_prev = prev_index + prev_len;
debug_assert!(
next_index >= end_of_prev,
"overlapping blocks: \n\
\t prev_index = {prev_index:#x}\n\
\t prev_len = {prev_len:#x}\n\
\tend_of_prev = {end_of_prev:#x}\n\
\t next_index = {next_index:#x}\n\
`next_index` should be >= `end_of_prev`"
);
let delta_to_next = next_index - end_of_prev;
delta_to_next < ALIGN_U32
}
#[inline]
fn round_u32_down_to_pow2(value: u32, divisor: u32) -> u32 {
debug_assert!(divisor > 0);
debug_assert!(divisor.is_power_of_two());
value & !(divisor - 1)
}
#[inline]
fn round_usize_down_to_pow2(value: usize, divisor: usize) -> usize {
debug_assert!(divisor > 0);
debug_assert!(divisor.is_power_of_two());
value & !(divisor - 1)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hash_map::HashMap;
use crate::prelude::*;
use proptest::prelude::*;
use std::num::NonZeroUsize;
fn free_list_block_len_and_size(free_list: &FreeList) -> (usize, Option<usize>) {
let len = free_list.num_free_blocks();
assert!(len <= 1);
let size = if free_list.bump_end > free_list.bump_ptr {
Some(usize::try_from(free_list.bump_end - free_list.bump_ptr).unwrap())
} else {
free_list
.free_block_index_to_len
.first_key_value()
.map(|(_, &s)| usize::try_from(s).unwrap())
};
(len, size)
}
proptest! {
#[test]
#[cfg_attr(miri, ignore)]
fn check_no_fragmentation((initial_capacity, ops) in ops()) {
let _ = env_logger::try_init();
log::trace!("------------------------------------------------------------------------");
let mut live = HashMap::new();
let mut deferred = vec![];
let mut free_list = FreeList::new(initial_capacity.get());
let (initial_len, initial_size) = free_list_block_len_and_size(&free_list);
assert!(initial_len == 0 || initial_len == 1);
assert!(initial_size.unwrap_or(0) <= initial_capacity.get());
assert_eq!(initial_size.unwrap_or(0), free_list.max_size());
let mut capacity = initial_capacity.get();
for op in ops {
match op {
Op::Alloc(id, layout) => {
if let Ok(Some(ptr)) = free_list.alloc(layout) {
assert_eq!(usize::try_from(ptr.get()).unwrap() % layout.align(), 0);
live.insert(id, ptr);
}
}
Op::Dealloc(id, layout) => {
if let Some(ptr) = live.remove(&id) {
free_list.dealloc(ptr, layout);
} else {
deferred.push((id, layout));
}
}
Op::AddCapacity(additional) => {
assert_eq!(capacity, free_list.current_capacity());
capacity = capacity.saturating_add(additional);
free_list.add_capacity(additional);
assert_eq!(capacity, free_list.current_capacity());
}
}
}
for (id, layout) in deferred {
if let Some(ptr) = live.remove(&id) {
free_list.dealloc(ptr, layout);
}
}
assert!(live.is_empty());
let (final_len, final_size) = free_list_block_len_and_size(&free_list);
if capacity == initial_capacity.get() {
assert_eq!(final_len, initial_len);
assert_eq!(final_size, initial_size);
} else {
assert!(capacity > initial_capacity.get());
assert!(final_len >= initial_len);
assert!(final_len <= 1);
assert!(final_size >= initial_size);
if let Some(final_size) = final_size {
assert!(final_size <= capacity);
if capacity >= 2 * ALIGN_USIZE {
let usable = capacity.min(usize::try_from(u32::MAX).unwrap());
assert!(final_size >= usable - 2 * ALIGN_USIZE, "assertion failed: {final_size} >= {usable} - 2 * {ALIGN_USIZE}");
}
}
}
}
}
#[derive(Clone, Debug)]
enum Op {
AddCapacity(usize),
Alloc(usize, Layout),
Dealloc(usize, Layout),
}
fn clamp_to_pow2_in_range(x: usize, max: usize) -> usize {
let log_x = max.ilog2() as usize;
if log_x == 0 {
return 1;
}
let divisor = usize::MAX / log_x;
let y = 1_usize << (x / divisor);
assert!(y.is_power_of_two(), "{y} is not a power of two");
assert!(y <= max, "{y} is larger than {max}");
y
}
fn arbitrary_layout(max_size: NonZeroUsize, size: usize, align: usize) -> Layout {
let max_size = std::cmp::min(max_size.get(), usize::try_from(isize::MAX).unwrap());
let align = clamp_to_pow2_in_range(align, super::ALIGN_USIZE);
let size = size % (max_size + 1);
let size = round_usize_down_to_pow2(size, align);
assert!(size <= max_size);
assert_ne!(align, 0);
assert!(align.is_power_of_two());
assert_eq!(size % align, 0);
assert!(size <= usize::try_from(isize::MAX).unwrap());
Layout::from_size_align(size, align).unwrap()
}
fn ops() -> impl Strategy<Value = (NonZeroUsize, Vec<Op>)> {
any::<usize>().prop_flat_map(|capacity| {
let capacity =
NonZeroUsize::new(capacity).unwrap_or_else(|| NonZeroUsize::new(1 << 31).unwrap());
(
Just(capacity),
(
any::<usize>(),
any::<usize>(),
any::<usize>(),
any::<usize>(),
)
.prop_flat_map(move |(id, size, align, additional)| {
let layout = arbitrary_layout(capacity, size, align);
vec![
Just(Op::Alloc(id, layout)),
Just(Op::Dealloc(id, layout)),
Just(Op::AddCapacity(additional)),
]
})
.prop_shuffle(),
)
})
}
#[test]
fn allocate_no_split() {
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 2);
assert_eq!(free_list.num_free_blocks(), 1);
assert_eq!(free_list.max_size(), ALIGN_USIZE * 2);
free_list
.alloc(Layout::from_size_align(ALIGN_USIZE + ALIGN_USIZE, ALIGN_USIZE).unwrap())
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
assert_eq!(free_list.num_free_blocks(), 0);
}
#[test]
fn allocate_and_split() {
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 3);
assert_eq!(free_list.num_free_blocks(), 1);
assert_eq!(free_list.max_size(), ALIGN_USIZE * 3);
free_list
.alloc(Layout::from_size_align(ALIGN_USIZE + ALIGN_USIZE, ALIGN_USIZE).unwrap())
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
assert_eq!(free_list.num_free_blocks(), 1);
}
#[test]
fn dealloc_merge_prev_and_next() {
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 100);
assert_eq!(
free_list.num_free_blocks(),
1,
"initially one big free block"
);
let a = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
assert_eq!(
free_list.num_free_blocks(),
1,
"should have split the block to allocate `a`"
);
let b = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
assert_eq!(
free_list.num_free_blocks(),
1,
"should have split the block to allocate `b`"
);
free_list.dealloc(a, layout);
assert_eq!(
free_list.num_free_blocks(),
2,
"should have two non-contiguous free blocks after deallocating `a`"
);
free_list.dealloc(b, layout);
assert_eq!(
free_list.num_free_blocks(),
1,
"should have merged `a` and `b` blocks with the rest to form a \
single, contiguous free block after deallocating `b`"
);
}
#[test]
fn dealloc_merge_with_prev_and_not_next() {
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 100);
assert_eq!(
free_list.num_free_blocks(),
1,
"initially one big free block"
);
let a = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
let b = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
let c = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
assert_eq!(
free_list.num_free_blocks(),
1,
"should have split the block to allocate `a`, `b`, and `c`"
);
free_list.dealloc(a, layout);
assert_eq!(
free_list.num_free_blocks(),
2,
"should have two non-contiguous free blocks after deallocating `a`"
);
free_list.dealloc(b, layout);
assert_eq!(
free_list.num_free_blocks(),
2,
"should have merged `a` and `b` blocks, but not merged with the \
rest of the free space"
);
let _ = c;
}
#[test]
fn dealloc_merge_with_next_and_not_prev() {
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 100);
assert_eq!(
free_list.num_free_blocks(),
1,
"initially one big free block"
);
let a = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
let b = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
let c = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
assert_eq!(
free_list.num_free_blocks(),
1,
"should have split the block to allocate `a`, `b`, and `c`"
);
free_list.dealloc(a, layout);
assert_eq!(
free_list.num_free_blocks(),
2,
"should have two non-contiguous free blocks after deallocating `a`"
);
free_list.dealloc(c, layout);
assert_eq!(
free_list.num_free_blocks(),
2,
"should have merged `c` block with rest of the free space, but not \
with `a` block"
);
let _ = b;
}
#[test]
fn dealloc_no_merge() {
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 100);
assert_eq!(
free_list.num_free_blocks(),
1,
"initially one big free block"
);
let a = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
let b = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
let c = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
let d = free_list
.alloc(layout)
.expect("allocation within 'static' free list limits")
.expect("have free space available for allocation");
assert_eq!(
free_list.num_free_blocks(),
1,
"should have split the block to allocate `a`, `b`, `c`, and `d`"
);
free_list.dealloc(a, layout);
assert_eq!(
free_list.num_free_blocks(),
2,
"should have two non-contiguous free blocks after deallocating `a`"
);
free_list.dealloc(c, layout);
assert_eq!(
free_list.num_free_blocks(),
3,
"should not have merged `c` block `a` block or rest of the free \
space"
);
let _ = (b, d);
}
#[test]
fn alloc_size_too_large() {
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 10);
assert_eq!(free_list.max_size(), ALIGN_USIZE * 10);
assert!(
free_list
.alloc(Layout::from_size_align(ALIGN_USIZE * 20, ALIGN_USIZE).unwrap())
.unwrap()
.is_none()
);
}
#[test]
fn alloc_align_too_large() {
let mut free_list = FreeList::new(ALIGN_USIZE + ALIGN_USIZE * 10);
assert_eq!(free_list.max_size(), ALIGN_USIZE * 10);
assert!(
free_list
.alloc(Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE * 2).unwrap(),)
.is_err()
);
}
#[test]
fn all_pairwise_alloc_dealloc_orderings() {
let tests: &[fn(&mut FreeList, Layout)] = &[
|f, l| {
let a = f.alloc(l).unwrap().unwrap();
let b = f.alloc(l).unwrap().unwrap();
f.dealloc(a, l);
f.dealloc(b, l);
},
|f, l| {
let a = f.alloc(l).unwrap().unwrap();
let b = f.alloc(l).unwrap().unwrap();
f.dealloc(b, l);
f.dealloc(a, l);
},
|f, l| {
let a = f.alloc(l).unwrap().unwrap();
f.dealloc(a, l);
let b = f.alloc(l).unwrap().unwrap();
f.dealloc(b, l);
},
];
let l = Layout::from_size_align(16, 8).unwrap();
for test in tests {
let mut f = FreeList::new(0x100);
test(&mut f, l);
}
}
#[test]
fn add_capacity() {
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
let mut free_list = FreeList::new(0);
assert!(free_list.alloc(layout).unwrap().is_none(), "no capacity");
free_list.add_capacity(ALIGN_USIZE);
assert!(
free_list.alloc(layout).unwrap().is_none(),
"still not enough capacity because we won't allocate the zero index"
);
free_list.add_capacity(1);
assert!(
free_list.alloc(layout).unwrap().is_none(),
"still not enough capacity because allocations are multiples of the alignment"
);
free_list.add_capacity(ALIGN_USIZE - 1);
let a = free_list
.alloc(layout)
.unwrap()
.expect("now we have enough capacity for one");
assert!(
free_list.alloc(layout).unwrap().is_none(),
"but not enough capacity for two"
);
free_list.add_capacity(ALIGN_USIZE);
let b = free_list
.alloc(layout)
.unwrap()
.expect("now we have enough capacity for two");
free_list.dealloc(a, layout);
free_list.dealloc(b, layout);
assert_eq!(
free_list.num_free_blocks(),
1,
"`dealloc` should merge blocks from different `add_capacity` calls together"
);
free_list.add_capacity(ALIGN_USIZE);
assert_eq!(
free_list.num_free_blocks(),
1,
"`add_capacity` should eagerly merge new capacity into the last block \
in the free list, when possible"
);
}
#[test]
fn add_capacity_not_enough_for_first_alloc() {
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
let mut free_list = FreeList::new(0);
assert!(free_list.alloc(layout).unwrap().is_none(), "no capacity");
for _ in 1..2 * ALIGN_USIZE {
free_list.add_capacity(1);
assert!(
free_list.alloc(layout).unwrap().is_none(),
"not enough capacity"
);
}
free_list.add_capacity(1);
free_list
.alloc(layout)
.unwrap()
.expect("now we have enough capacity for one");
assert!(
free_list.alloc(layout).unwrap().is_none(),
"but not enough capacity for two"
);
}
#[test]
fn bump_ptr_overflow() {
if core::mem::size_of::<usize>() < core::mem::size_of::<u64>() {
return;
}
let capacity = usize::try_from(u32::MAX).unwrap();
let len = round_usize_down_to_pow2(capacity, ALIGN_USIZE) - ALIGN_USIZE;
let mut free_list = FreeList::new(capacity);
assert_eq!(free_list.num_free_blocks(), 1);
let layout = Layout::from_size_align(len - ALIGN_USIZE, ALIGN_USIZE).unwrap();
free_list.alloc(layout).unwrap().unwrap();
assert_eq!(free_list.num_free_blocks(), 1);
assert!(free_list.bump_ptr.checked_add(2 * ALIGN_U32).is_none());
let layout = Layout::from_size_align(2 * ALIGN_USIZE, ALIGN_USIZE).unwrap();
assert!(free_list.alloc(layout).unwrap().is_none());
assert_eq!(free_list.num_free_blocks(), 1);
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
free_list.alloc(layout).unwrap().unwrap();
assert_eq!(free_list.num_free_blocks(), 0);
let layout = Layout::from_size_align(ALIGN_USIZE, ALIGN_USIZE).unwrap();
assert!(free_list.alloc(layout).unwrap().is_none());
assert_eq!(free_list.num_free_blocks(), 0);
}
}