use std::alloc::Layout;
use std::cell::Cell;
use crate::descriptor::TypeDescriptor;
use crate::gc::GcHeader;
use crate::heap::BlockLayout;
pub(crate) const PAGE_SIZE: usize = 1 << 15;
pub(crate) const PAGE_MASK: usize = !(PAGE_SIZE - 1);
pub(crate) const BLOCK_GRANULE: usize = std::mem::align_of::<GcHeader>();
pub(crate) const MIN_BLOCK: usize = std::mem::size_of::<GcHeader>();
pub(crate) const MAX_BLOCK: usize = 128;
pub(crate) const NUM_CLASSES: usize = (MAX_BLOCK - MIN_BLOCK) / BLOCK_GRANULE + 1;
const MAX_BLOCKS: usize = PAGE_SIZE / MIN_BLOCK;
const BITS_PER_WORD: usize = u64::BITS as usize;
const BITMAP_WORDS: usize = MAX_BLOCKS.div_ceil(BITS_PER_WORD);
const CLASS_LARGE: u8 = u8::MAX;
const FLAG_IMMORTAL: u8 = 1;
const _: () = {
assert!(PAGE_SIZE.is_power_of_two());
assert!(MIN_BLOCK.is_multiple_of(BLOCK_GRANULE));
assert!(MAX_BLOCK.is_multiple_of(BLOCK_GRANULE));
assert!(MAX_BLOCK >= MIN_BLOCK);
assert!(NUM_CLASSES < CLASS_LARGE as usize);
};
const fn round_up_to_multiple(n: usize, m: usize) -> usize {
n.div_ceil(m) * m
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub(crate) struct SizeClass(u8);
impl SizeClass {
#[inline]
pub(crate) const fn of(block: BlockLayout) -> Option<SizeClass> {
if block.align == BLOCK_GRANULE && block.size <= MAX_BLOCK {
let size = if block.size < MIN_BLOCK {
MIN_BLOCK
} else {
block.size
};
let index = (round_up_to_multiple(size, BLOCK_GRANULE) - MIN_BLOCK) / BLOCK_GRANULE;
Some(SizeClass(index as u8))
} else {
None
}
}
#[inline]
pub(crate) const fn index(self) -> usize {
self.0 as usize
}
#[inline]
pub(crate) const fn block_size(self) -> usize {
MIN_BLOCK + self.0 as usize * BLOCK_GRANULE
}
#[cfg(test)]
#[inline]
pub(crate) fn from_index(index: usize) -> SizeClass {
assert!(index < NUM_CLASSES);
SizeClass(index as u8)
}
}
#[repr(C)]
pub(crate) struct PageHeader {
heap_id: Cell<u32>,
block_size: Cell<u32>,
first_block: Cell<u32>,
block_count: Cell<u32>,
last_word: Cell<u32>,
tail_mask: Cell<u64>,
payload_offset: Cell<u32>,
recip: Cell<u32>,
page_bytes: Cell<u32>,
live_count: Cell<u32>,
cursor: Cell<u32>,
class: Cell<u8>,
flags: Cell<u8>,
_pad: [u8; 2],
next: Cell<*mut PageHeader>,
next_of_class: Cell<*mut PageHeader>,
allocated: [Cell<u64>; BITMAP_WORDS],
mark: [Cell<u64>; BITMAP_WORDS],
}
const _: () = {
let first = round_up_to_multiple(std::mem::size_of::<PageHeader>(), MIN_BLOCK);
assert!(first < PAGE_SIZE);
assert!((PAGE_SIZE - first) / MIN_BLOCK <= BITMAP_WORDS * BITS_PER_WORD);
};
#[inline]
pub(crate) fn page_of(p: *const u8) -> *mut PageHeader {
((p as usize) & PAGE_MASK) as *mut PageHeader
}
impl PageHeader {
pub(crate) const CURSOR_OFFSET: usize = core::mem::offset_of!(PageHeader, cursor);
pub(crate) const LAST_WORD_OFFSET: usize = core::mem::offset_of!(PageHeader, last_word);
pub(crate) const ALLOCATED_OFFSET: usize = core::mem::offset_of!(PageHeader, allocated);
pub(crate) const LIVE_COUNT_OFFSET: usize = core::mem::offset_of!(PageHeader, live_count);
pub(crate) const fn first_block_of(block_size: usize) -> usize {
round_up_to_multiple(std::mem::size_of::<PageHeader>(), block_size)
}
pub(crate) fn new_small(class: SizeClass, heap_id: u32) -> *mut PageHeader {
let block_size = class.block_size();
let first_block = Self::first_block_of(block_size);
let block_count = (PAGE_SIZE - first_block) / block_size;
let payload_offset = GcHeader::payload_offset_for(BLOCK_GRANULE);
Self::alloc_page(
PAGE_SIZE,
heap_id,
class.index() as u8,
block_size,
first_block,
block_count,
payload_offset,
)
}
pub(crate) fn new_large(
descriptor: &TypeDescriptor,
payload_offset: usize,
block: BlockLayout,
heap_id: u32,
) -> *mut PageHeader {
assert!(
block.align < PAGE_SIZE,
"payload alignment {} of descriptor {} exceeds the largest alignment \
a GC page can place",
descriptor.align(),
descriptor.name
);
let first_block = round_up_to_multiple(std::mem::size_of::<PageHeader>(), block.align);
let page_bytes = round_up_to_multiple(first_block + block.size, PAGE_SIZE);
Self::alloc_page(
page_bytes,
heap_id,
CLASS_LARGE,
block.size,
first_block,
1,
payload_offset,
)
}
#[allow(clippy::too_many_arguments)]
fn alloc_page(
page_bytes: usize,
heap_id: u32,
class: u8,
block_size: usize,
first_block: usize,
block_count: usize,
payload_offset: usize,
) -> *mut PageHeader {
debug_assert!(block_count <= BITMAP_WORDS * BITS_PER_WORD);
debug_assert!(first_block + block_count * block_size <= page_bytes);
let page_bytes_u32 =
u32::try_from(page_bytes).expect("a GC page larger than 4 GiB is not a page");
let layout = Layout::from_size_align(page_bytes, PAGE_SIZE).expect("valid page layout");
let raw = unsafe { std::alloc::alloc(layout) };
if raw.is_null() {
std::alloc::handle_alloc_error(layout);
}
let page = raw as *mut PageHeader;
unsafe {
std::ptr::write(
page,
PageHeader {
heap_id: Cell::new(heap_id),
block_size: Cell::new(block_size as u32),
first_block: Cell::new(first_block as u32),
block_count: Cell::new(block_count as u32),
last_word: Cell::new(last_word_for(block_count)),
tail_mask: Cell::new(tail_mask_for(block_count)),
payload_offset: Cell::new(payload_offset as u32),
recip: Cell::new(reciprocal(block_size)),
page_bytes: Cell::new(page_bytes_u32),
live_count: Cell::new(0),
cursor: Cell::new(0),
class: Cell::new(class),
flags: Cell::new(0),
_pad: [0; 2],
next: Cell::new(std::ptr::null_mut()),
next_of_class: Cell::new(std::ptr::null_mut()),
allocated: std::array::from_fn(|_| Cell::new(0)),
mark: std::array::from_fn(|_| Cell::new(0)),
},
);
}
page
}
pub(crate) unsafe fn release(page: *mut PageHeader) {
let bytes = unsafe { (*page).page_bytes.get() } as usize;
let layout = Layout::from_size_align(bytes, PAGE_SIZE).expect("valid page layout");
unsafe { std::alloc::dealloc(page as *mut u8, layout) };
}
pub(crate) fn reclass(&self, class: SizeClass) {
assert_eq!(self.live_count.get(), 0, "reclassing a non-empty page");
assert_ne!(self.class.get(), CLASS_LARGE, "reclassing a large page");
let block_size = class.block_size();
let first_block = Self::first_block_of(block_size);
let block_count = (PAGE_SIZE - first_block) / block_size;
self.class.set(class.index() as u8);
self.block_size.set(block_size as u32);
self.first_block.set(first_block as u32);
self.block_count.set(block_count as u32);
self.last_word.set(last_word_for(block_count));
self.tail_mask.set(tail_mask_for(block_count));
self.recip.set(reciprocal(block_size));
self.payload_offset
.set(GcHeader::payload_offset_for(BLOCK_GRANULE) as u32);
self.cursor.set(0);
}
#[inline]
pub(crate) fn base(&self) -> *mut u8 {
self as *const PageHeader as *mut u8
}
#[inline]
pub(crate) fn block_ptr(&self, index: usize) -> *mut u8 {
debug_assert!(index < self.block_count.get() as usize);
unsafe {
self.base()
.add(self.first_block.get() as usize + index * self.block_size.get() as usize)
}
}
#[inline]
pub(crate) fn block_index(&self, p: *const u8) -> usize {
let offset = (p as usize & !PAGE_MASK) - self.first_block.get() as usize;
debug_assert_eq!(
offset % self.block_size.get() as usize,
0,
"an address that is not a block base"
);
let index = ((offset as u64 * self.recip.get() as u64) >> 32) as usize;
debug_assert_eq!(index, offset / self.block_size.get() as usize);
debug_assert!(index < self.block_count.get() as usize);
index
}
#[inline]
pub(crate) fn block_index_in(&self, word: usize, bit: u32) -> usize {
let index = word * BITS_PER_WORD + bit as usize;
debug_assert!(index < self.block_count.get() as usize);
index
}
#[inline]
pub(crate) fn claim_free_block(&self) -> Option<*mut u8> {
let last = self.last_word.get() as usize;
let mut w = self.cursor.get() as usize;
while w <= last {
let taken = self.allocated[w].get();
let free = if w == last {
!taken & self.tail_mask.get()
} else {
!taken
};
if free != 0 {
let bit = free.trailing_zeros();
self.allocated[w].set(taken | (1u64 << bit));
self.cursor.set(w as u32);
self.live_count.set(self.live_count.get() + 1);
return Some(self.block_ptr(self.block_index_in(w, bit)));
}
w += 1;
}
self.cursor.set((last + 1) as u32);
None
}
#[inline]
pub(crate) fn is_allocated(&self, index: usize) -> bool {
self.allocated[index / BITS_PER_WORD].get() & (1u64 << (index % BITS_PER_WORD)) != 0
}
#[inline]
pub(crate) fn test_and_set_mark(&self, index: usize) -> bool {
let word = index / BITS_PER_WORD;
let bit = 1u64 << (index % BITS_PER_WORD);
let current = self.mark[word].get();
self.mark[word].set(current | bit);
current & bit != 0
}
#[inline]
pub(crate) fn words(&self) -> usize {
self.last_word.get() as usize + 1
}
#[cfg(test)]
fn valid_mask(&self, word: usize) -> u64 {
let start = word * BITS_PER_WORD;
let count = self.block_count.get() as usize;
if start + BITS_PER_WORD <= count {
u64::MAX
} else if start >= count {
0
} else {
(1u64 << (count - start)) - 1
}
}
#[inline]
pub(crate) fn allocated_word(&self, word: usize) -> u64 {
self.allocated[word].get()
}
#[inline]
pub(crate) fn set_allocated_word(&self, word: usize, value: u64) {
self.allocated[word].set(value);
}
#[inline]
pub(crate) fn fits_large(&self, payload_offset: usize, block: BlockLayout) -> bool {
self.class().is_none()
&& self.block_size() == block.size
&& self.payload_offset() == payload_offset
&& self.first_block.get() as usize
== round_up_to_multiple(std::mem::size_of::<PageHeader>(), block.align)
}
#[inline]
pub(crate) fn mark_word(&self, word: usize) -> u64 {
self.mark[word].get()
}
#[inline]
pub(crate) fn clear_mark_word(&self, word: usize) {
self.mark[word].set(0);
}
pub(crate) fn clear_bitmaps(&self) {
for word in 0..BITMAP_WORDS {
self.allocated[word].set(0);
self.mark[word].set(0);
}
self.live_count.set(0);
self.cursor.set(0);
}
#[inline]
pub(crate) fn heap_id(&self) -> u32 {
self.heap_id.get()
}
#[inline]
pub(crate) fn set_heap_id(&self, id: u32) {
self.heap_id.set(id);
}
#[inline]
pub(crate) fn block_size(&self) -> usize {
self.block_size.get() as usize
}
#[inline]
pub(crate) fn payload_offset(&self) -> usize {
self.payload_offset.get() as usize
}
#[cfg(test)]
#[inline]
pub(crate) fn first_block(&self) -> usize {
self.first_block.get() as usize
}
#[inline]
pub(crate) fn block_count(&self) -> usize {
self.block_count.get() as usize
}
#[inline]
pub(crate) fn live_count(&self) -> u32 {
self.live_count.get()
}
#[inline]
pub(crate) fn release_blocks(&self, freed: u32) {
self.live_count.set(self.live_count.get() - freed);
}
#[inline]
pub(crate) fn page_bytes(&self) -> usize {
self.page_bytes.get() as usize
}
#[inline]
pub(crate) fn rewind_cursor(&self) {
self.cursor.set(0);
}
#[inline]
pub(crate) fn class(&self) -> Option<SizeClass> {
let class = self.class.get();
if class == CLASS_LARGE {
None
} else {
Some(SizeClass(class))
}
}
#[inline]
pub(crate) fn is_immortal(&self) -> bool {
self.flags.get() & FLAG_IMMORTAL != 0
}
#[inline]
pub(crate) fn set_immortal(&self) {
self.flags.set(self.flags.get() | FLAG_IMMORTAL);
}
#[inline]
pub(crate) fn clear_immortal(&self) {
self.flags.set(self.flags.get() & !FLAG_IMMORTAL);
}
#[inline]
pub(crate) fn next(&self) -> *mut PageHeader {
self.next.get()
}
#[inline]
pub(crate) fn set_next(&self, page: *mut PageHeader) {
self.next.set(page);
}
#[inline]
pub(crate) fn next_of_class(&self) -> *mut PageHeader {
self.next_of_class.get()
}
#[inline]
pub(crate) fn set_next_of_class(&self, page: *mut PageHeader) {
self.next_of_class.set(page);
}
}
#[inline]
fn reciprocal(block_size: usize) -> u32 {
((1u64 << 32) / block_size as u64) as u32 + 1
}
fn last_word_for(block_count: usize) -> u32 {
debug_assert!(block_count > 0);
(block_count.div_ceil(BITS_PER_WORD) - 1) as u32
}
fn tail_mask_for(block_count: usize) -> u64 {
match block_count % BITS_PER_WORD {
0 => u64::MAX,
tail => (1u64 << tail) - 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
struct OwnedPage(*mut PageHeader);
impl OwnedPage {
fn small(class: SizeClass) -> OwnedPage {
OwnedPage(PageHeader::new_small(class, 1))
}
fn get(&self) -> &PageHeader {
unsafe { &*self.0 }
}
}
impl Drop for OwnedPage {
fn drop(&mut self) {
unsafe { PageHeader::release(self.0) };
}
}
#[test]
fn the_ladder_covers_every_builtin_descriptor() {
for descriptor in crate::descriptor::BUILTINS {
let (payload_offset, block) = BlockLayout::of(descriptor);
let class = SizeClass::of(block).unwrap_or_else(|| {
panic!(
"descriptor {} has block {{size: {}, align: {}}}, which the \
ladder does not hold (MIN_BLOCK = {MIN_BLOCK}, MAX_BLOCK = \
{MAX_BLOCK}, BLOCK_GRANULE = {BLOCK_GRANULE})",
descriptor.name, block.size, block.align
)
});
assert!(
class.block_size() >= block.size,
"class {} is too small for {}",
class.index(),
descriptor.name
);
assert!(
class.block_size() - block.size < BLOCK_GRANULE,
"class {} wastes a whole granule on {}",
class.index(),
descriptor.name
);
assert_eq!(
payload_offset,
GcHeader::payload_offset_for(BLOCK_GRANULE),
"every small block starts its payload at the page's offset"
);
}
}
#[test]
fn an_int_block_is_the_header_plus_eight() {
let (payload_offset, block) = BlockLayout::of(&crate::scalars::INT);
assert_eq!(payload_offset, 16, "the header, and no padding");
assert_eq!(block.size, 24, "16 bytes of header and 8 of payload");
assert_eq!(block.align, BLOCK_GRANULE);
let class = SizeClass::of(block).expect("an Int is on the ladder");
assert_eq!(
class.block_size(),
24,
"an Int must land on a rung that is exactly its block, not above it"
);
}
#[test]
fn the_reciprocal_divides_exactly_for_every_stride_and_offset() {
for index in 0..NUM_CLASSES {
let stride = SizeClass::from_index(index).block_size();
let recip = reciprocal(stride);
for offset in 0..PAGE_SIZE {
let derived = ((offset as u64 * recip as u64) >> 32) as usize;
assert_eq!(
derived,
offset / stride,
"reciprocal for stride {stride} disagrees with division at offset {offset}"
);
}
}
}
#[test]
fn every_block_round_trips_through_the_mask_and_the_index() {
for index in 0..NUM_CLASSES {
let class = SizeClass::from_index(index);
let page = OwnedPage::small(class);
let p = page.get();
assert!(p.block_count() > 0);
for block in 0..p.block_count() {
let address = p.block_ptr(block);
assert_eq!(
page_of(address),
page.0,
"class {index} block {block} masked to the wrong page"
);
assert_eq!(
p.block_index(address),
block,
"class {index} block {block} indexed wrong"
);
assert_eq!(
address as usize % BLOCK_GRANULE,
0,
"every block base must be header-aligned"
);
}
let end = p.first_block.get() as usize + p.block_count() * p.block_size();
assert!(end <= p.page_bytes());
}
}
#[test]
fn first_block_is_a_multiple_of_block_size_and_clears_the_header() {
for index in 0..NUM_CLASSES {
let class = SizeClass::from_index(index);
let page = OwnedPage::small(class);
let p = page.get();
let first = p.first_block.get() as usize;
assert_eq!(first % p.block_size(), 0, "class {index}");
assert!(first >= std::mem::size_of::<PageHeader>(), "class {index}");
assert!(p.block_count() <= BITMAP_WORDS * 64, "class {index}");
}
}
#[test]
fn claiming_exhausts_a_page_exactly_block_count_times() {
let class = SizeClass::from_index(0);
let page = OwnedPage::small(class);
let p = page.get();
let mut claimed = Vec::new();
while let Some(block) = p.claim_free_block() {
claimed.push(block);
}
assert_eq!(claimed.len(), p.block_count());
assert_eq!(p.live_count(), p.block_count() as u32);
assert!(p.claim_free_block().is_none(), "a full page stays full");
for pair in claimed.windows(2) {
assert_eq!(pair[1] as usize - pair[0] as usize, p.block_size());
}
for (index, block) in claimed.iter().enumerate() {
assert!(p.is_allocated(index));
assert_eq!(p.block_index(*block), index);
}
}
#[test]
fn the_bitmap_tail_never_names_a_block() {
for index in 0..NUM_CLASSES {
let class = SizeClass::from_index(index);
let page = OwnedPage::small(class);
let p = page.get();
let mut total = 0u32;
for word in 0..p.words() {
total += p.valid_mask(word).count_ones();
}
assert_eq!(total, p.block_count() as u32, "class {index}");
assert_eq!(p.valid_mask(p.words()), 0, "class {index}");
assert_eq!(p.last_word.get() as usize, p.words() - 1, "class {index}");
assert_eq!(
p.tail_mask.get(),
p.valid_mask(p.words() - 1),
"class {index}"
);
}
}
#[test]
fn the_folded_first_block_is_the_one_every_page_of_the_class_has() {
for index in 0..NUM_CLASSES {
let class = SizeClass::from_index(index);
let page = OwnedPage::small(class);
let p = page.get();
assert_eq!(
p.first_block(),
PageHeader::first_block_of(class.block_size()),
"a fresh page of class {index}"
);
for other in (0..NUM_CLASSES).rev() {
let other_class = SizeClass::from_index(other);
p.reclass(other_class);
assert_eq!(
p.first_block(),
PageHeader::first_block_of(other_class.block_size()),
"class {index} re-classed to {other}"
);
assert_eq!(p.block_size(), other_class.block_size());
}
}
}
#[test]
fn a_freed_block_is_reclaimed_lowest_first() {
let page = OwnedPage::small(SizeClass::from_index(0));
let p = page.get();
let first = p.claim_free_block().expect("a fresh page has room");
let second = p.claim_free_block().expect("a fresh page has room");
p.set_allocated_word(0, p.allocated_word(0) & !1);
p.release_blocks(1);
p.rewind_cursor();
assert_eq!(
p.claim_free_block().expect("the hole is claimable"),
first,
"the lowest hole must be reused first"
);
assert_ne!(first, second);
}
#[test]
fn marking_reports_the_previous_state_and_starts_clear() {
let page = OwnedPage::small(SizeClass::from_index(1));
let p = page.get();
for index in [0usize, 1, 63, 64, 65] {
assert!(!p.test_and_set_mark(index), "the bitmap starts clear");
assert!(p.test_and_set_mark(index), "the second visit sees the bit");
}
p.clear_mark_word(0);
assert!(!p.test_and_set_mark(0));
assert!(p.test_and_set_mark(64), "clearing word 0 left word 1 alone");
}
#[test]
fn a_reclassed_page_takes_the_new_geometry() {
let page = OwnedPage::small(SizeClass::from_index(0));
let p = page.get();
let before = p.block_count();
let target = SizeClass::from_index(NUM_CLASSES - 1);
p.reclass(target);
assert_eq!(p.block_size(), MAX_BLOCK);
assert!(p.block_count() < before);
assert_eq!(p.class(), Some(target));
let block = p.claim_free_block().expect("a reclassed page has room");
assert_eq!(p.block_index(block), 0);
assert_eq!(page_of(block), page.0);
}
#[test]
fn a_large_page_places_an_overaligned_payload_at_its_alignment() {
for align in [64usize, 256, 4096, PAGE_SIZE / 2] {
let block = BlockLayout {
size: GcHeader::payload_offset_for(align) + 8,
align,
};
let payload_offset = GcHeader::payload_offset_for(align);
let raw = PageHeader::new_large(&crate::scalars::INT, payload_offset, block, 0);
let p = unsafe { &*raw };
assert_eq!(p.block_count(), 1);
assert_eq!(p.class(), None);
let base = p.claim_free_block().expect("a large page has one block");
assert_eq!(base as usize % align, 0, "align {align}");
let payload = unsafe { base.add(payload_offset) };
assert_eq!(payload as usize % align, 0, "align {align}");
assert_eq!(page_of(base), raw, "the header stays in the first unit");
assert_eq!(p.block_index(base), 0);
assert!(p.claim_free_block().is_none());
unsafe { PageHeader::release(raw) };
}
}
#[test]
#[should_panic(expected = "exceeds the largest alignment a GC page can place")]
fn an_alignment_a_page_cannot_place_is_a_panic_naming_the_descriptor() {
let block = BlockLayout {
size: PAGE_SIZE + 8,
align: PAGE_SIZE,
};
let _ = PageHeader::new_large(&crate::scalars::INT, PAGE_SIZE, block, 0);
}
#[test]
fn the_ladder_rejects_over_alignment_and_over_size() {
assert_eq!(
SizeClass::of(BlockLayout {
size: 48,
align: BLOCK_GRANULE
})
.map(SizeClass::block_size),
Some(48)
);
assert_eq!(
SizeClass::of(BlockLayout {
size: 48,
align: 16
}),
None,
"same size, stricter alignment: not the same page"
);
assert_eq!(
SizeClass::of(BlockLayout {
size: MAX_BLOCK + 1,
align: BLOCK_GRANULE
}),
None,
"one past the ladder is a large page, not rung zero"
);
assert_eq!(
SizeClass::of(BlockLayout {
size: 1,
align: BLOCK_GRANULE
})
.map(SizeClass::index),
Some(0)
);
for size in MIN_BLOCK..=MAX_BLOCK {
let class = SizeClass::of(BlockLayout {
size,
align: BLOCK_GRANULE,
})
.expect("inside the ladder");
assert!(class.block_size() >= size);
assert!(class.index() < NUM_CLASSES);
}
}
#[test]
fn distinct_pages_mask_to_distinct_bases() {
let a = OwnedPage::small(SizeClass::from_index(0));
let b = OwnedPage::small(SizeClass::from_index(0));
assert_ne!(a.0, b.0);
assert_eq!(a.0 as usize % PAGE_SIZE, 0);
assert_eq!(b.0 as usize % PAGE_SIZE, 0);
let block = b.get().claim_free_block().expect("room");
assert_eq!(page_of(block), b.0);
assert_ne!(page_of(block), a.0);
}
}