use super::chunkfooter::{ChunkFooter, EMPTY_CHUNK};
use core::cmp::Ordering as MemOrdering;
use std::alloc::{Layout, alloc};
use std::fmt;
use std::iter;
use std::mem;
use std::ptr::NonNull;
use std::slice;
use std::sync::Mutex;
use std::{
alloc::dealloc,
ptr,
sync::atomic::{AtomicPtr, AtomicUsize, Ordering as SyncOrdering},
};
const TYPICAL_PAGE_SIZE: usize = 0x1000;
const MALLOC_OVERHEAD: usize = 16;
const SUPPORTED_ITER_ALIGNMENT: usize = 16;
const CHUNK_ALIGN: usize = SUPPORTED_ITER_ALIGNMENT;
const FOOTER_SIZE: usize = mem::size_of::<ChunkFooter>();
const _FOOTER_ALIGN_ASSERTION: () = {
assert!(mem::align_of::<ChunkFooter>() <= CHUNK_ALIGN);
};
const OVERHEAD: usize = match round_up_to(MALLOC_OVERHEAD + FOOTER_SIZE, CHUNK_ALIGN) {
Some(x) => x,
None => panic!(),
};
const FIRST_ALLOCATION_GOAL: usize = 1 << 9;
const DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER: usize = FIRST_ALLOCATION_GOAL - OVERHEAD;
#[derive(Debug)]
pub struct SyncBump<const MIN_ALIGN: usize = 1> {
current_chunkfooter: AtomicPtr<ChunkFooter>,
allocation_limit: AtomicUsize,
slow_path_lock: std::sync::Mutex<()>,
}
impl<const MIN_ALIGN: usize> Default for SyncBump<MIN_ALIGN> {
fn default() -> Self {
Self::with_min_align()
}
}
impl<const MIN_ALIGN: usize> Drop for SyncBump<MIN_ALIGN> {
fn drop(&mut self) {
unsafe {
dealloc_chunk_list(self.current_chunkfooter.load(SyncOrdering::SeqCst));
}
}
}
unsafe fn dealloc_chunk_list(footer: *mut ChunkFooter) {
let mut current_ptr = footer;
let iterator = iter::from_fn(move || {
let current_ref = unsafe { &*current_ptr };
if current_ref.is_empty() {
current_ptr = ptr::null_mut();
return None;
}
let ptr_to_return = current_ptr;
current_ptr = current_ref.prev.as_ptr();
Some(ptr_to_return)
});
iterator.for_each(|chunk_ptr| {
let chunk_ref = unsafe { &*chunk_ptr };
unsafe {
dealloc(chunk_ref.bottom.as_ptr(), chunk_ref.layout);
}
});
}
impl<const MIN_ALIGN: usize> SyncBump<MIN_ALIGN> {
pub fn with_min_align() -> Self {
assert!(MIN_ALIGN.is_power_of_two());
assert!(MIN_ALIGN <= CHUNK_ALIGN);
SyncBump {
current_chunkfooter: AtomicPtr::new(unsafe { EMPTY_CHUNK.get_ptr() }),
allocation_limit: AtomicUsize::new(usize::MAX),
slow_path_lock: Mutex::new(()),
}
}
pub fn with_capacity(capacity: usize) -> Self {
Self::try_with_capacity(capacity).unwrap_or_else(|_| oom())
}
pub fn try_with_capacity(capacity: usize) -> Result<Self, AllocErr> {
Self::try_with_min_align_and_capacity(capacity)
}
pub fn try_with_min_align_and_capacity(capacity: usize) -> Result<Self, AllocErr> {
assert!(
MIN_ALIGN.is_power_of_two(),
"MIN_ALIGN must be a power of two; found {MIN_ALIGN}"
);
assert!(
MIN_ALIGN <= CHUNK_ALIGN,
"MIN_ALIGN may not be larger than {CHUNK_ALIGN}; found {MIN_ALIGN}"
);
if capacity == 0 {
return Ok(SyncBump {
current_chunkfooter: AtomicPtr::new(unsafe { EMPTY_CHUNK.get_ptr() }),
allocation_limit: AtomicUsize::new(usize::MAX),
slow_path_lock: Mutex::new(()),
});
}
let layout = layout_from_size_align(capacity, MIN_ALIGN)?;
let chunk_footer = unsafe {
Self::new_chunk(
Self::new_chunk_memory_details(None, layout).ok_or(AllocErr)?,
layout,
NonNull::new_unchecked(EMPTY_CHUNK.get_ptr()),
)
.ok_or(AllocErr)?
};
Ok(SyncBump {
current_chunkfooter: AtomicPtr::new(chunk_footer.as_ptr()),
allocation_limit: AtomicUsize::new(usize::MAX),
slow_path_lock: Mutex::new(()),
})
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_str(&self, src: &str) -> &mut str {
let buffer = self.alloc_slice_copy(src.as_bytes());
unsafe {
str::from_utf8_unchecked_mut(buffer)
}
}
#[inline(always)]
pub fn alloc_layout(&self, layout: Layout) -> NonNull<u8> {
self.try_alloc_layout(layout).unwrap_or_else(|_| oom())
}
#[inline(always)]
pub fn try_alloc_layout(&self, layout: Layout) -> Result<NonNull<u8>, AllocErr> {
if let Some(p) = self.try_alloc_layout_fast(layout) {
Ok(p)
} else {
self.alloc_layout_slow(layout).ok_or(AllocErr)
}
}
fn try_alloc_layout_fast(&self, layout: Layout) -> Option<NonNull<u8>> {
let chunk_ptr = self.current_chunkfooter.load(SyncOrdering::Acquire);
if unsafe { (*chunk_ptr).is_empty() } {
return None;
}
let chunk_ref = unsafe { &*chunk_ptr };
let start = chunk_ref.bottom.as_ptr();
loop {
let ptr = chunk_ref.top.load(SyncOrdering::Acquire);
let aligned_ptr = match layout.align().cmp(&MIN_ALIGN) {
MemOrdering::Less => {
let aligned_size = round_up_to(layout.size(), MIN_ALIGN)?;
if aligned_size > (ptr as usize).wrapping_sub(start as usize) {
return None; }
ptr.wrapping_sub(aligned_size)
}
MemOrdering::Equal => {
let aligned_size =
unsafe { round_up_to_unchecked(layout.size(), layout.align()) };
if aligned_size > (ptr as usize).wrapping_sub(start as usize) {
return None;
}
ptr.wrapping_sub(aligned_size)
}
MemOrdering::Greater => {
let aligned_size =
unsafe { round_up_to_unchecked(layout.size(), layout.align()) };
let aligned_ptr_candidate = round_mut_ptr_down_to(ptr, layout.align());
if aligned_ptr_candidate < start
|| aligned_size
> (aligned_ptr_candidate as usize).wrapping_sub(start as usize)
{
return None;
}
aligned_ptr_candidate.wrapping_sub(aligned_size)
}
};
match chunk_ref.top.compare_exchange(
ptr, aligned_ptr, SyncOrdering::AcqRel, SyncOrdering::Acquire, ) {
Ok(_) => {
return Some(unsafe { NonNull::new_unchecked(aligned_ptr) });
}
Err(_) => {
continue;
}
}
}
}
#[inline(never)]
#[cold]
fn alloc_layout_slow(&self, layout: Layout) -> Option<NonNull<u8>> {
let _guard = self.slow_path_lock.lock().unwrap();
if let Some(ptr) = self.try_alloc_layout_fast(layout) {
return Some(ptr);
}
unsafe {
let allocation_limit_remaining = self.allocation_limit_remaining();
let current_chunk_ptr = self.current_chunkfooter.load(SyncOrdering::Acquire);
let current_chunk_ref = &*current_chunk_ptr;
let current_layout = current_chunk_ref.layout;
let min_new_chunk_size = layout.size().max(DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER);
let mut base_size = (current_layout.size() - FOOTER_SIZE)
.checked_mul(2)?
.max(min_new_chunk_size);
let chunk_memory_details = iter::from_fn(|| {
let bypass_min_chunk_size_for_small_limits = matches!(self.allocation_limit(), Some(limit) if layout.size() < limit
&& base_size >= layout.size()
&& limit < DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER
&& self.allocated_bytes() == 0);
if base_size >= min_new_chunk_size || bypass_min_chunk_size_for_small_limits {
let size = base_size;
base_size /= 2;
Self::new_chunk_memory_details(Some(size), layout)
} else {
None
}
});
let new_footer_ptr = chunk_memory_details
.filter_map(|chunk_memory_details| {
if Self::chunk_fits_under_limit(
allocation_limit_remaining,
chunk_memory_details,
) {
let prev_non_null = NonNull::new(current_chunk_ptr)
.expect("BUG: current_chunk pointer should never be null.");
Self::new_chunk(chunk_memory_details, layout, prev_non_null)
} else {
None
}
})
.next()?;
debug_assert_eq!(
new_footer_ptr.as_ref().bottom.as_ptr() as usize % layout.align(),
0
);
self.current_chunkfooter
.store(new_footer_ptr.as_ptr(), SyncOrdering::Release);
let ptr = self.try_alloc_layout_fast(layout);
debug_assert!(ptr.is_some());
ptr
}
}
fn allocation_limit_remaining(&self) -> Option<usize> {
let limit = self.allocation_limit.load(SyncOrdering::Acquire);
if limit == usize::MAX {
return None;
}
let allocated = self.allocated_bytes();
if allocated > limit {
None
} else {
Some(limit - allocated) }
}
pub fn allocated_bytes(&self) -> usize {
let footer_ptr = self.current_chunkfooter.load(SyncOrdering::Acquire);
unsafe {
debug_assert!(!footer_ptr.is_null());
(*footer_ptr).allocated_bytes
}
}
pub fn allocation_limit(&self) -> Option<usize> {
match self.allocation_limit.load(SyncOrdering::Acquire) {
usize::MAX => None,
limit => Some(limit),
}
}
fn new_chunk_memory_details(
new_size_without_footer: Option<usize>,
requested_layout: Layout,
) -> Option<NewChunkMemoryDetails> {
let align = CHUNK_ALIGN.max(MIN_ALIGN).max(requested_layout.align());
let mut new_size_without_footer =
new_size_without_footer.unwrap_or(DEFAULT_CHUNK_SIZE_WITHOUT_FOOTER);
let requested_size =
round_up_to(requested_layout.size(), align).unwrap_or_else(allocation_size_overflow);
new_size_without_footer = new_size_without_footer.max(requested_size);
if new_size_without_footer < TYPICAL_PAGE_SIZE {
new_size_without_footer =
(new_size_without_footer + OVERHEAD).next_power_of_two() - OVERHEAD;
} else {
new_size_without_footer =
round_up_to(new_size_without_footer + OVERHEAD, TYPICAL_PAGE_SIZE)? - OVERHEAD;
}
debug_assert_eq!(align % CHUNK_ALIGN, 0);
debug_assert_eq!(new_size_without_footer % CHUNK_ALIGN, 0);
let size = new_size_without_footer
.checked_add(FOOTER_SIZE)
.unwrap_or_else(allocation_size_overflow);
Some(NewChunkMemoryDetails {
new_size_without_footer,
size,
align,
})
}
unsafe fn new_chunk(
new_chunk_memory_details: NewChunkMemoryDetails,
requested_layout: Layout,
prev: NonNull<ChunkFooter>,
) -> Option<NonNull<ChunkFooter>> {
let NewChunkMemoryDetails {
new_size_without_footer,
align,
size,
} = new_chunk_memory_details;
let layout = layout_from_size_align(size, align).ok()?;
debug_assert!(size >= requested_layout.size());
let bottom = unsafe { alloc(layout) };
let bottom = NonNull::new(bottom)?;
let footer_ptr = unsafe { bottom.as_ptr().add(new_size_without_footer) };
debug_assert_eq!((bottom.as_ptr() as usize) % align, 0);
debug_assert_eq!(footer_ptr as usize % CHUNK_ALIGN, 0);
let footer_ptr = footer_ptr as *mut ChunkFooter;
let ptr = round_mut_ptr_down_to(footer_ptr.cast::<u8>(), MIN_ALIGN);
debug_assert_eq!(ptr as usize % MIN_ALIGN, 0);
debug_assert!(
bottom.as_ptr() < ptr,
"bump pointer {ptr:#p} should still be greater than or equal to the \
start of the bump chunk {bottom:#p}"
);
debug_assert_eq!(
(ptr as usize) - (bottom.as_ptr() as usize),
new_size_without_footer
);
let top = AtomicPtr::new(ptr);
let allocated_bytes = unsafe { prev.as_ref().allocated_bytes + new_size_without_footer };
unsafe {
ptr::write(
footer_ptr,
ChunkFooter {
bottom,
layout,
prev,
top,
allocated_bytes,
},
)
};
Some(unsafe { NonNull::new_unchecked(footer_ptr) })
}
#[allow(clippy::mut_from_ref)]
fn chunk_fits_under_limit(
allocation_limit_remaining: Option<usize>,
new_chunk_memory_details: NewChunkMemoryDetails,
) -> bool {
allocation_limit_remaining
.map(|allocation_limit_left| {
allocation_limit_left >= new_chunk_memory_details.new_size_without_footer
})
.unwrap_or(true)
}
#[inline(always)]
#[allow(clippy::mut_from_ref)]
pub fn alloc_slice_copy<T>(&self, src: &[T]) -> &mut [T]
where
T: Copy,
{
let layout = Layout::for_value(src);
let dst = self.alloc_layout(layout).cast::<T>();
unsafe {
ptr::copy_nonoverlapping(src.as_ptr(), dst.as_ptr(), src.len());
slice::from_raw_parts_mut(dst.as_ptr(), src.len())
}
}
}
#[derive(Debug, Clone, Copy)]
struct NewChunkMemoryDetails {
new_size_without_footer: usize,
align: usize,
size: usize,
}
#[cold]
#[inline(never)]
fn allocation_size_overflow<T>() -> T {
panic!("requested allocation size overflowed")
}
#[inline]
fn layout_from_size_align(size: usize, align: usize) -> Result<Layout, AllocErr> {
Layout::from_size_align(size, align).map_err(|_| AllocErr)
}
#[inline]
const fn round_up_to(n: usize, divisor: usize) -> Option<usize> {
debug_assert!(divisor > 0);
debug_assert!(divisor.is_power_of_two());
match n.checked_add(divisor - 1) {
Some(x) => Some(x & !(divisor - 1)),
None => None,
}
}
#[inline]
unsafe fn round_up_to_unchecked(n: usize, divisor: usize) -> usize {
match round_up_to(n, divisor) {
Some(x) => x,
None => {
debug_assert!(false, "round_up_to_unchecked failed");
unsafe { core::hint::unreachable_unchecked() }
}
}
}
#[inline]
fn round_mut_ptr_down_to(ptr: *mut u8, divisor: usize) -> *mut u8 {
debug_assert!(divisor > 0);
debug_assert!(divisor.is_power_of_two());
ptr.wrapping_sub(ptr as usize & (divisor - 1))
}
#[inline(never)]
#[cold]
fn oom() -> ! {
panic!("out of memory")
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct AllocErr;
impl fmt::Display for AllocErr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("memory allocation failed")
}
}