From 12dcae32fb54323c26b77164a7554c8c6b45aec9 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Thu, 9 Jan 2025 12:24:23 +0100 Subject: [PATCH 01/36] allocator trait --- betree/src/allocator.rs | 204 ---------------- betree/src/allocator/mod.rs | 276 ++++++++++++++++++++++ betree/src/allocator/segment_allocator.rs | 79 +++++++ betree/src/data_management/dmu.rs | 4 +- betree/src/database/handler.rs | 23 +- betree/src/database/mod.rs | 6 + 6 files changed, 377 insertions(+), 215 deletions(-) delete mode 100644 betree/src/allocator.rs create mode 100644 betree/src/allocator/mod.rs create mode 100644 betree/src/allocator/segment_allocator.rs diff --git a/betree/src/allocator.rs b/betree/src/allocator.rs deleted file mode 100644 index 04f7c73f4..000000000 --- a/betree/src/allocator.rs +++ /dev/null @@ -1,204 +0,0 @@ -//! This module provides `SegmentAllocator` and `SegmentId` for bitmap -//! allocation of 1GiB segments. - -use crate::{cow_bytes::CowBytes, storage_pool::DiskOffset, vdev::Block, Error}; -use bitvec::prelude::*; -use byteorder::{BigEndian, ByteOrder}; -use std::io::Write; - -/// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` -pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; -/// Number of bytes required to store a segments allocation bitmap -pub const SEGMENT_SIZE_BYTES: usize = SEGMENT_SIZE / 8; -const SEGMENT_SIZE_LOG_2: usize = 18; -const SEGMENT_SIZE_MASK: usize = SEGMENT_SIZE - 1; - -/// Simple first-fit bitmap allocator -pub struct SegmentAllocator { - data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), -} - -impl SegmentAllocator { - /// Constructs a new `SegmentAllocator` given the segment allocation bitmap. - /// The `bitmap` must have a length of `SEGMENT_SIZE`. - pub fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { - SegmentAllocator { - data: BitArray::new(bitmap), - } - } - - /// Allocates a block of the given `size`. - /// Returns `None` if the allocation request cannot be satisfied. - pub fn allocate(&mut self, size: u32) -> Option { - if size == 0 { - return Some(0); - } - let offset = { - let mut idx = 0; - loop { - loop { - if idx + size > SEGMENT_SIZE as u32 { - return None; - } - if !self.data[idx as usize] { - break; - } - idx += 1; - } - - let start_idx = (idx + 1) as usize; - let end_idx = (idx + size) as usize; - if let Some(first_alloc_idx) = self.data[start_idx..end_idx].first_one() { - idx = (idx + 1) + first_alloc_idx as u32 + 1; - } else { - break idx; - } - } - }; - self.mark(offset, size, Action::Allocate); - return Some(offset); - } - - /// Allocates a block of the given `size` at `offset`. - /// Returns `false` if the allocation request cannot be satisfied. - pub fn allocate_at(&mut self, size: u32, offset: u32) -> bool { - if size == 0 { - return true; - } - if offset + size > SEGMENT_SIZE as u32 { - return false; - } - - let start_idx = offset as usize; - let end_idx = (offset + size) as usize; - if self.data[start_idx..end_idx].any() { - return false; - } - self.mark(offset, size, Action::Allocate); - true - } - - /// Deallocates the allocated block. - pub fn deallocate(&mut self, offset: u32, size: u32) { - log::debug!( - "Marked a block range {{ offset: {}, size: {} }} for deallocation", - offset, - size - ); - self.mark(offset, size, Action::Deallocate); - } - - fn mark(&mut self, offset: u32, size: u32, action: Action) { - let start_idx = offset as usize; - let end_idx = (offset + size) as usize; - let range = &mut self.data[start_idx..end_idx]; - - match action { - // Is allocation, so range must be free - Action::Allocate => debug_assert!(!range.any()), - // Is deallocation, so range must be previously used - Action::Deallocate => debug_assert!(range.all()), - } - - range.fill(action.as_bool()); - } -} - -// TODO better wording -/// Allocation action -#[derive(Clone, Copy)] -pub enum Action { - /// Deallocate an allocated block. - Deallocate, - /// Allocate a deallocated block. - Allocate, -} - -impl Action { - /// Returns 1 if allocation and 0 if deallocation. - pub fn as_bool(self) -> bool { - match self { - Action::Deallocate => false, - Action::Allocate => true, - } - } -} - -/// Identifier for 1GiB segments of a `StoragePool`. -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SegmentId(pub u64); - -impl SegmentId { - /// Returns the corresponding segment of the given disk offset. - pub fn get(offset: DiskOffset) -> Self { - SegmentId(offset.as_u64() & !(SEGMENT_SIZE_MASK as u64)) - } - - /// Returns the block offset into the segment. - pub fn get_block_offset(offset: DiskOffset) -> u32 { - offset.as_u64() as u32 & SEGMENT_SIZE_MASK as u32 - } - - /// Returns the disk offset at the start of this segment. - pub fn as_disk_offset(&self) -> DiskOffset { - DiskOffset::from_u64(self.0) - } - - /// Returns the disk offset of the block in this segment at the given - /// offset. - pub fn disk_offset(&self, segment_offset: u32) -> DiskOffset { - DiskOffset::from_u64(self.0 + u64::from(segment_offset)) - } - - /// Returns the key of this segment for messages and queries. - pub fn key(&self, key_prefix: &[u8]) -> CowBytes { - // Shave off the two lower bytes because they are always null. - let mut segment_key = [0; 8]; - BigEndian::write_u64(&mut segment_key[..], self.0); - assert_eq!(&segment_key[6..], &[0, 0]); - - let mut key = CowBytes::new(); - key.push_slice(key_prefix); - key.push_slice(&segment_key[..6]); - key - } - - /// Returns the ID of the disk that belongs to this segment. - pub fn disk_id(&self) -> u16 { - self.as_disk_offset().disk_id() - } - - /// Returns the next segment ID. - /// Wraps around at the end of the disk. - pub fn next(&self, disk_size: Block) -> SegmentId { - let disk_offset = self.as_disk_offset(); - if disk_offset.block_offset().as_u64() + SEGMENT_SIZE as u64 >= disk_size.as_u64() { - SegmentId::get(DiskOffset::new( - disk_offset.storage_class(), - disk_offset.disk_id(), - Block(0), - )) - } else { - SegmentId(self.0 + SEGMENT_SIZE as u64) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn segment_id() { - let offset = DiskOffset::new(1, 2, Block::from_bytes(4096)); - let segment = SegmentId::get(offset); - - assert_eq!(segment.as_disk_offset().storage_class(), 1); - assert_eq!(segment.disk_id(), 2); - assert_eq!( - segment.as_disk_offset().block_offset(), - Block::from_bytes(0) - ); - assert_eq!(SegmentId::get_block_offset(offset), 1); - } -} diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs new file mode 100644 index 000000000..ec2262d4f --- /dev/null +++ b/betree/src/allocator/mod.rs @@ -0,0 +1,276 @@ +use crate::{cow_bytes::CowBytes, storage_pool::DiskOffset, vdev::Block}; +use bitvec::prelude::*; +use byteorder::{BigEndian, ByteOrder}; +use serde::{Deserialize, Serialize}; + +mod segment_allocator; +pub use self::segment_allocator::SegmentAllocator; + +/// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` +pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; +/// Number of bytes required to store a segments allocation bitmap +pub const SEGMENT_SIZE_BYTES: usize = SEGMENT_SIZE / 8; +const SEGMENT_SIZE_LOG_2: usize = 18; +const SEGMENT_SIZE_MASK: usize = SEGMENT_SIZE - 1; + +/// The `AllocatorType` enum represents different strategies for allocating blocks +/// of memory within a fixed-size segment. +#[derive(Debug, Serialize, Deserialize, Clone, Copy)] +#[serde(rename_all = "lowercase")] // This will deserialize "firstfit" as FirstFit +pub enum AllocatorType { + /// **Segment Allocator:** + /// This is a first fit allocator that was used before making the allocators + /// generic. It is not efficient and mainly included for reference. + SegmentAllocator, +} + +/// The `Allocator` trait defines an interface for allocating and deallocating +/// blocks of memory within a fixed-size segment. Different allocators can +/// implement various strategies for managing free space within the segment. +pub trait Allocator: Send + Sync { + /// Accesses the underlying bitmap data. + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0); + + /// Constructs a new `Allocator` instance given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self + where + Self: Sized; + + /// Allocates a block of memory of the given `size`. + /// + /// This method attempts to find a contiguous free block of memory within the + /// segment that is large enough to satisfy the request. + fn allocate(&mut self, size: u32) -> Option; + + /// Allocates a block of memory of the given `size` at the specified `offset`. + /// + /// This method attempts to allocate a contiguous block of memory at the given offset. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool; + + /// Deallocates a previously allocated block of memory. + /// + /// This method marks the specified block as free, making it available for + /// future allocations. + fn deallocate(&mut self, offset: u32, size: u32); + + /// Marks a range of bits in the bitmap with the given action. + fn mark(&mut self, offset: u32, size: u32, action: Action) { + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + let range = &mut self.data()[start_idx..end_idx]; + + match action { + Action::Allocate => debug_assert!(!range.any()), + Action::Deallocate => debug_assert!(range.all()), + } + + range.fill(action.as_bool()); + } +} + +// TODO better wording +/// Allocation action +#[derive(Clone, Copy)] +pub enum Action { + /// Deallocate an allocated block. + Deallocate, + /// Allocate a deallocated block. + Allocate, +} + +impl Action { + /// Returns 1 if allocation and 0 if deallocation. + pub fn as_bool(self) -> bool { + match self { + Action::Deallocate => false, + Action::Allocate => true, + } + } +} + +/// Identifier for 1GiB segments of a `StoragePool`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SegmentId(pub u64); + +impl SegmentId { + /// Returns the corresponding segment of the given disk offset. + pub fn get(offset: DiskOffset) -> Self { + SegmentId(offset.as_u64() & !(SEGMENT_SIZE_MASK as u64)) + } + + /// Returns the block offset into the segment. + pub fn get_block_offset(offset: DiskOffset) -> u32 { + offset.as_u64() as u32 & SEGMENT_SIZE_MASK as u32 + } + + /// Returns the disk offset at the start of this segment. + pub fn as_disk_offset(&self) -> DiskOffset { + DiskOffset::from_u64(self.0) + } + + /// Returns the disk offset of the block in this segment at the given + /// offset. + pub fn disk_offset(&self, segment_offset: u32) -> DiskOffset { + DiskOffset::from_u64(self.0 + u64::from(segment_offset)) + } + + /// Returns the key of this segment for messages and queries. + pub fn key(&self, key_prefix: &[u8]) -> CowBytes { + // Shave off the two lower bytes because they are always null. + let mut segment_key = [0; 8]; + BigEndian::write_u64(&mut segment_key[..], self.0); + assert_eq!(&segment_key[6..], &[0, 0]); + + let mut key = CowBytes::new(); + key.push_slice(key_prefix); + key.push_slice(&segment_key[..6]); + key + } + + /// Returns the ID of the disk that belongs to this segment. + pub fn disk_id(&self) -> u16 { + self.as_disk_offset().disk_id() + } + + /// Returns the next segment ID. + /// Wraps around at the end of the disk. + pub fn next(&self, disk_size: Block) -> SegmentId { + let disk_offset = self.as_disk_offset(); + if disk_offset.block_offset().as_u64() + SEGMENT_SIZE as u64 >= disk_size.as_u64() { + SegmentId::get(DiskOffset::new( + disk_offset.storage_class(), + disk_offset.disk_id(), + Block(0), + )) + } else { + SegmentId(self.0 + SEGMENT_SIZE as u64) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::{ + distributions::WeightedIndex, prelude::Distribution, rngs::StdRng, Rng, SeedableRng, + }; + + #[test] + fn segment_id() { + let offset = DiskOffset::new(1, 2, Block::from_bytes(4096)); + let segment = SegmentId::get(offset); + + assert_eq!(segment.as_disk_offset().storage_class(), 1); + assert_eq!(segment.disk_id(), 2); + assert_eq!( + segment.as_disk_offset().block_offset(), + Block::from_bytes(0) + ); + assert_eq!(SegmentId::get_block_offset(offset), 1); + } + + fn run_fuzz_tests(allocator: &mut T) { + let mut rng = StdRng::seed_from_u64(0); + + // Store allocation records (offset, size) + let mut allocations: Vec<(u32, u32)> = Vec::new(); + + // Define action weights (allocate: 60%, deallocate: 30%, allocate_at: 10%) + let weights = [60, 30, 10]; + let dist = WeightedIndex::new(&weights).unwrap(); + + for _ in 0..100000 { + let action = dist.sample(&mut rng); + match action { + 0 => { + // Allocate + let size = rng.gen_range(1..256); + if let Some(offset) = allocator.allocate(size) { + allocations.push((offset, size)); + } + } + 1 => { + // Deallocate + if !allocations.is_empty() { + let index = rng.gen_range(0..allocations.len()); + let (offset, size) = allocations.swap_remove(index); // Remove the chosen allocation + allocator.deallocate(offset, size); + } + } + 2 => { + // Allocate at + let offset = rng.gen_range(0..(SEGMENT_SIZE * 2) as u32); + let size = rng.gen_range(1..256); + if allocator.allocate_at(offset, size) { + allocations.push((offset, size)); // Store if successful + } + } + _ => unreachable!(), + } + } + + // TODO: find some generic assertions + } + + macro_rules! generate_fuzz_tests { + ($test_name:ident, $allocator_type:ty) => { + #[test] + fn $test_name() { + let mut allocator = <$allocator_type>::new([0; SEGMENT_SIZE_BYTES]); + run_fuzz_tests(&mut allocator); + } + }; + } + + fn small_tests(allocator: &mut T) { + let offset = allocator.allocate(10); + assert!(offset.is_some()); + + let offset = offset.unwrap(); + allocator.deallocate(offset, 10); + + let success = allocator.allocate_at(5, 10); + assert!(success); + + // Allocation Failure + let offset = allocator.allocate(SEGMENT_SIZE as u32); // Assuming the entire segment isn't free + assert!(offset.is_none()); + + // Out-of-bounds + let success = allocator.allocate_at(SEGMENT_SIZE as u32, 1); + assert!(!success); + + // Zero-sized allocation + let offset = allocator.allocate(0); + assert!(offset.is_some()); + + // Repeated Allocation/Deallocation + for _ in 0..10 { + let offset = allocator.allocate(12).unwrap(); + allocator.deallocate(offset, 12); + } + + // 8. Large Allocation Followed by Small Allocations + let large_offset = allocator.allocate(100).unwrap(); + _ = allocator.allocate(4).unwrap(); + _ = allocator.allocate(7).unwrap(); + allocator.deallocate(large_offset, 100); + } + + macro_rules! generate_small_tests { + ($test_name:ident, $allocator_type:ty) => { + #[test] + fn $test_name() { + let mut allocator = <$allocator_type>::new([0; SEGMENT_SIZE_BYTES]); + small_tests(&mut allocator); + } + }; + } + + // Generate tests for each allocator + generate_small_tests!(test_segment_allocator, SegmentAllocator); + + // Generate fuzz tests for each allocator + generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); +} diff --git a/betree/src/allocator/segment_allocator.rs b/betree/src/allocator/segment_allocator.rs new file mode 100644 index 000000000..51a628f2e --- /dev/null +++ b/betree/src/allocator/segment_allocator.rs @@ -0,0 +1,79 @@ +use super::*; + +/// Simple first-fit that is used for reference. For a more efficient version see FirstFit. +pub struct SegmentAllocator { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), +} + +impl Allocator for SegmentAllocator { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `FirstFit` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + SegmentAllocator { + data: BitArray::new(bitmap), + } + } + + /// Allocates a block of the given `size`. + /// Returns `None` if the allocation request cannot be satisfied and the offset if if can. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + let offset = { + let mut idx = 0; + loop { + loop { + if idx + size > SEGMENT_SIZE as u32 { + return None; + } + if !self.data[idx as usize] { + break; + } + idx += 1; + } + + let start_idx = (idx + 1) as usize; + let end_idx = (idx + size) as usize; + if let Some(first_alloc_idx) = self.data[start_idx..end_idx].first_one() { + idx = (idx + 1) + first_alloc_idx as u32 + 1; + } else { + break idx; + } + } + }; + self.mark(offset, size, Action::Allocate); + return Some(offset); + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + true + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + } +} diff --git a/betree/src/data_management/dmu.rs b/betree/src/data_management/dmu.rs index 8b5a27eb1..023034b84 100644 --- a/betree/src/data_management/dmu.rs +++ b/betree/src/data_management/dmu.rs @@ -6,7 +6,7 @@ use super::{ CopyOnWriteEvent, Dml, HasStoragePreference, Object, ObjectReference, }; use crate::{ - allocator::{Action, SegmentAllocator, SegmentId, SEGMENT_SIZE}, + allocator::{Action, Allocator, SegmentAllocator, SegmentId, SEGMENT_SIZE}, buffer::Buf, cache::{Cache, ChangeKeyError, RemoveError}, checksum::{Builder, Checksum, State}, @@ -40,6 +40,8 @@ use std::{ thread::yield_now, }; +type DefaultAllocator = SegmentAllocator; + /// The Data Management Unit. pub struct Dmu where diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 71bbbcab6..2f417d049 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -4,7 +4,7 @@ use super::{ AtomicStorageInfo, DatasetId, DeadListData, Generation, StorageInfo, TreeInner, }; use crate::{ - allocator::{Action, SegmentAllocator, SegmentId, SEGMENT_SIZE_BYTES}, + allocator::*, atomic_option::AtomicOption, cow_bytes::SlicedCowBytes, data_management::{CopyOnWriteEvent, Dml, HasStoragePreference, ObjectReference}, @@ -53,12 +53,13 @@ pub struct Handler { pub(crate) free_space_tier: Vec, pub(crate) delayed_messages: Mutex, SlicedCowBytes)>>, pub(crate) last_snapshot_generation: RwLock>, + pub(crate) allocator: AllocatorType, // Cache for allocators which have been in use since the last sync. This is // done to avoid cyclical updates on evictions. // NOTE: This map needs to be updated/emptied on sync's as the internal // representation is not updated on deallocation to avoid overwriting // potentially valid fallback data. - pub(crate) allocators: RwLock>>, + pub(crate) allocators: RwLock>>>, pub(crate) allocations: AtomicU64, pub(crate) old_root_allocation: SeqLock)>>, } @@ -95,13 +96,13 @@ impl Handler { } } -pub struct SegmentAllocatorGuard<'a> { - inner: RwLockReadGuard<'a, HashMap>>, +pub struct AllocatorGuard<'a> { + inner: RwLockReadGuard<'a, HashMap>>>, id: SegmentId, } -impl<'a> SegmentAllocatorGuard<'a> { - pub fn access(&self) -> RwLockWriteGuard { +impl<'a> AllocatorGuard<'a> { + pub fn access(&self) -> RwLockWriteGuard> { self.inner.get(&self.id).unwrap().write() } } @@ -159,7 +160,7 @@ impl Handler { Ok(()) } - pub fn get_allocation_bitmap(&self, id: SegmentId, dmu: &X) -> Result + pub fn get_allocation_bitmap(&self, id: SegmentId, dmu: &X) -> Result where X: Dml, ObjectRef = OR, ObjectPointer = OR::ObjectPointer>, { @@ -167,7 +168,7 @@ impl Handler { // Test if bitmap is already in cache let foo = self.allocators.read(); if foo.contains_key(&id) { - return Ok(SegmentAllocatorGuard { inner: foo, id }); + return Ok(AllocatorGuard { inner: foo, id }); } } @@ -194,7 +195,9 @@ impl Handler { } } - let mut allocator = SegmentAllocator::new(bitmap); + let mut allocator: Box = match self.allocator { + AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), + }; if let Some((offset, size)) = self.old_root_allocation.read() { if SegmentId::get(offset) == id { @@ -207,7 +210,7 @@ impl Handler { self.allocators.write().insert(id, RwLock::new(allocator)); let foo = self.allocators.read(); - Ok(SegmentAllocatorGuard { inner: foo, id }) + Ok(AllocatorGuard { inner: foo, id }) } pub fn free_space_disk(&self, disk_id: GlobalDiskId) -> Option { diff --git a/betree/src/database/mod.rs b/betree/src/database/mod.rs index bc9f37e0f..070805167 100644 --- a/betree/src/database/mod.rs +++ b/betree/src/database/mod.rs @@ -1,5 +1,6 @@ //! This module provides the Database Layer. use crate::{ + allocator::*, atomic_option::AtomicOption, cache::ClockCache, checksum::GxHash, @@ -150,6 +151,9 @@ pub struct DatabaseConfiguration { /// Where to log the allocations pub allocation_log_file_path: PathBuf, + + /// Select the allocator to use + pub allocator: AllocatorType, } impl Default for DatabaseConfiguration { @@ -166,6 +170,7 @@ impl Default for DatabaseConfiguration { metrics: None, migration_policy: None, allocation_log_file_path: PathBuf::from("allocation_log.bin"), + allocator: AllocatorType::SegmentAllocator, } } } @@ -212,6 +217,7 @@ impl DatabaseConfiguration { free_space_tier: (0..NUM_STORAGE_CLASSES) .map(|_| AtomicStorageInfo::default()) .collect_vec(), + allocator: self.allocator, allocations: AtomicU64::new(0), old_root_allocation: SeqLock::new(None), allocators: RwLock::new(HashMap::new()), From e5cb35f4ae6ea92a5eed63419a96101c45c38603 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Thu, 9 Jan 2025 12:28:56 +0100 Subject: [PATCH 02/36] first fit --- betree/src/allocator/first_fit.rs | 76 +++++++++++++++++++++++++++++++ betree/src/allocator/mod.rs | 10 ++++ betree/src/database/handler.rs | 1 + 3 files changed, 87 insertions(+) create mode 100644 betree/src/allocator/first_fit.rs diff --git a/betree/src/allocator/first_fit.rs b/betree/src/allocator/first_fit.rs new file mode 100644 index 000000000..745147052 --- /dev/null +++ b/betree/src/allocator/first_fit.rs @@ -0,0 +1,76 @@ +use super::*; + +/// Simple first-fit bitmap allocator +pub struct FirstFit { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), +} + +impl Allocator for FirstFit { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `FirstFit` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + FirstFit { + data: BitArray::new(bitmap), + } + } + + /// Allocates a block of the given `size`. + /// Returns `None` if the allocation request cannot be satisfied and the offset if if can. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + let mut offset = 0; + + while offset + size <= SEGMENT_SIZE as u32 { + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + + match self.data[start_idx..end_idx].last_one() { + Some(last_alloc_idx) => { + // Skip to the end of the last allocated block if there is any one at all. + offset += last_alloc_idx as u32 + 1 + } + None => { + // No allocated blocks found, so allocate here. + self.mark(offset, size, Action::Allocate); + return Some(offset); + } + } + } + + None + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + true + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + } +} diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index ec2262d4f..599ef9a8c 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -3,6 +3,9 @@ use bitvec::prelude::*; use byteorder::{BigEndian, ByteOrder}; use serde::{Deserialize, Serialize}; +mod first_fit; +pub use self::first_fit::FirstFit; + mod segment_allocator; pub use self::segment_allocator::SegmentAllocator; @@ -18,6 +21,11 @@ const SEGMENT_SIZE_MASK: usize = SEGMENT_SIZE - 1; #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[serde(rename_all = "lowercase")] // This will deserialize "firstfit" as FirstFit pub enum AllocatorType { + /// **First Fit:** + /// This allocator searches the segment from the beginning and allocates the + /// first free block that is large enough to satisfy the request. + FirstFit, + /// **Segment Allocator:** /// This is a first fit allocator that was used before making the allocators /// generic. It is not efficient and mainly included for reference. @@ -269,8 +277,10 @@ mod tests { } // Generate tests for each allocator + generate_small_tests!(test_first_fit, FirstFit); generate_small_tests!(test_segment_allocator, SegmentAllocator); // Generate fuzz tests for each allocator + generate_fuzz_tests!(test_first_fit_fuzz, FirstFit); generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); } diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 2f417d049..e7948c3fd 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -196,6 +196,7 @@ impl Handler { } let mut allocator: Box = match self.allocator { + AllocatorType::FirstFit => Box::new(FirstFit::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From b6acfa1dca4d71d0516b22d4a88617e58225716e Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Thu, 9 Jan 2025 12:30:36 +0100 Subject: [PATCH 03/36] next fit --- betree/src/allocator/mod.rs | 10 ++++ betree/src/allocator/next_fit.rs | 97 ++++++++++++++++++++++++++++++++ betree/src/database/handler.rs | 1 + 3 files changed, 108 insertions(+) create mode 100644 betree/src/allocator/next_fit.rs diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 599ef9a8c..23d4b552f 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -6,6 +6,9 @@ use serde::{Deserialize, Serialize}; mod first_fit; pub use self::first_fit::FirstFit; +mod next_fit; +pub use self::next_fit::NextFit; + mod segment_allocator; pub use self::segment_allocator::SegmentAllocator; @@ -26,6 +29,11 @@ pub enum AllocatorType { /// first free block that is large enough to satisfy the request. FirstFit, + /// **Next Fit:** + /// This allocator starts searching from the last allocation and continues + /// searching the segment for the next free block that is large enough. + NextFit, + /// **Segment Allocator:** /// This is a first fit allocator that was used before making the allocators /// generic. It is not efficient and mainly included for reference. @@ -278,9 +286,11 @@ mod tests { // Generate tests for each allocator generate_small_tests!(test_first_fit, FirstFit); + generate_small_tests!(test_next_fit, NextFit); generate_small_tests!(test_segment_allocator, SegmentAllocator); // Generate fuzz tests for each allocator generate_fuzz_tests!(test_first_fit_fuzz, FirstFit); + generate_fuzz_tests!(test_next_fit_fuzz, NextFit); generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); } diff --git a/betree/src/allocator/next_fit.rs b/betree/src/allocator/next_fit.rs new file mode 100644 index 000000000..e3fb89800 --- /dev/null +++ b/betree/src/allocator/next_fit.rs @@ -0,0 +1,97 @@ +use super::*; + +/// Simple next-fit bitmap allocator +pub struct NextFit { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + last_offset: u32, +} + +impl Allocator for NextFit { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `NextFit` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + NextFit { + data: BitArray::new(bitmap), + last_offset: 0, + } + } + + /// Allocates a block of the given `size`. + /// Returns `None` if the allocation request cannot be satisfied. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + let mut offset = self.last_offset; + let mut wrap_around = false; + + loop { + if offset + size > SEGMENT_SIZE as u32 { + // Wrap around to the beginning of the segment. + // NOTE: We **can't** set offset here. + wrap_around = true; + } + + if wrap_around && offset >= self.last_offset { + // We've circled back to the starting point, no space found + return None; + } + + if offset + size > SEGMENT_SIZE as u32 { + // NOTE: We **can't** set offset above because of the case if self.last_offset is + // larger than SEGMENT_SIZE - size. If it is and we would set offset above we would + // run into an infinite loop. Because the `offset >= self.last_offset` condition + // would never be fulfilled because offset is reset beforehand. + offset = 0; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + + match self.data[start_idx..end_idx].last_one() { + Some(last_alloc_idx) => { + // Skip to the end of the last allocated block if there is any one at all. + offset += last_alloc_idx as u32 + 1 + } + None => { + // No allocated blocks found, so allocate here. + self.mark(offset, size, Action::Allocate); + self.last_offset = offset + size + 1; + return Some(offset); + } + } + } + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + self.last_offset = offset + size; + true + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + } +} diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index e7948c3fd..ce3df57ff 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -197,6 +197,7 @@ impl Handler { let mut allocator: Box = match self.allocator { AllocatorType::FirstFit => Box::new(FirstFit::new(bitmap)), + AllocatorType::NextFit => Box::new(NextFit::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From f9733fef80e9f1a9eac3ba9494664b4cc7fcc847 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Thu, 9 Jan 2025 12:32:23 +0100 Subject: [PATCH 04/36] best fit simple --- betree/src/allocator/best_fit_simple.rs | 109 ++++++++++++++++++++++++ betree/src/allocator/mod.rs | 11 +++ betree/src/database/handler.rs | 1 + 3 files changed, 121 insertions(+) create mode 100644 betree/src/allocator/best_fit_simple.rs diff --git a/betree/src/allocator/best_fit_simple.rs b/betree/src/allocator/best_fit_simple.rs new file mode 100644 index 000000000..918bf07e8 --- /dev/null +++ b/betree/src/allocator/best_fit_simple.rs @@ -0,0 +1,109 @@ +use super::*; + +/// Simple best-fit bitmap allocator +pub struct BestFitSimple { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), +} + +impl Allocator for BestFitSimple { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `BestFitSimple` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + BestFitSimple { + data: BitArray::new(bitmap), + } + } + + /// Allocates a block of the given `size`. + /// Returns `None` if the allocation request cannot be satisfied. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + let mut best_fit_offset = None; + // Initialize with a value larger than any possible size + let mut best_fit_size = SEGMENT_SIZE as u32 + 1; + let mut offset: u32 = 0; + + while offset + size <= SEGMENT_SIZE as u32 { + let end_idx = (offset + size) as usize; + + match self.data[offset as usize..end_idx].last_one() { + Some(last_alloc_idx) => { + // Skip to the end of the last allocated block + offset += last_alloc_idx as u32 + 1; + } + None => { + // Find the next allocated block after the current free range + match self.data[end_idx..].first_one() { + Some(next_alloc_idx) => { + let free_block_size = next_alloc_idx as u32 + end_idx as u32 - offset; + + // Check if this free block is a better fit + if free_block_size >= size && free_block_size < best_fit_size { + best_fit_offset = Some(offset); + best_fit_size = free_block_size; + // If this free block is optimal finish iterating + if free_block_size == size { + break; + } + } + + offset = next_alloc_idx as u32 + end_idx as u32 + 1; + } + None => { + // No more allocated blocks, we have scanned the whole segment. + let free_block_size = self.data[offset as usize..].len() as u32; + + // Check if this free block is a better fit + if free_block_size >= size && free_block_size < best_fit_size { + best_fit_offset = Some(offset); + best_fit_size = free_block_size; + } + + break; + } + } + } + } + } + + if let Some(offset) = best_fit_offset { + self.mark(offset, size, Action::Allocate); + } + + best_fit_offset + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + true + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + } +} diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 23d4b552f..793707fec 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -9,6 +9,9 @@ pub use self::first_fit::FirstFit; mod next_fit; pub use self::next_fit::NextFit; +mod best_fit_simple; +pub use self::best_fit_simple::BestFitSimple; + mod segment_allocator; pub use self::segment_allocator::SegmentAllocator; @@ -34,6 +37,12 @@ pub enum AllocatorType { /// searching the segment for the next free block that is large enough. NextFit, + /// **Best Fit (Simple):** + /// This allocator searches the entire segment and allocates the smallest + /// free block that is large enough to satisfy the request. This simple + /// version uses a linear search to find the best fit. + BestFitSimple, + /// **Segment Allocator:** /// This is a first fit allocator that was used before making the allocators /// generic. It is not efficient and mainly included for reference. @@ -287,10 +296,12 @@ mod tests { // Generate tests for each allocator generate_small_tests!(test_first_fit, FirstFit); generate_small_tests!(test_next_fit, NextFit); + generate_small_tests!(test_best_fit_simple, BestFitSimple); generate_small_tests!(test_segment_allocator, SegmentAllocator); // Generate fuzz tests for each allocator generate_fuzz_tests!(test_first_fit_fuzz, FirstFit); generate_fuzz_tests!(test_next_fit_fuzz, NextFit); + generate_fuzz_tests!(test_best_fit_simple_fuzz, BestFitSimple); generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); } diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index ce3df57ff..4f92e8cde 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -198,6 +198,7 @@ impl Handler { let mut allocator: Box = match self.allocator { AllocatorType::FirstFit => Box::new(FirstFit::new(bitmap)), AllocatorType::NextFit => Box::new(NextFit::new(bitmap)), + AllocatorType::BestFitSimple => Box::new(BestFitSimple::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From 82b947bce7911099201f5ff5e66a5dd4769feb0b Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Thu, 9 Jan 2025 12:33:38 +0100 Subject: [PATCH 05/36] worst fit simple --- betree/src/allocator/mod.rs | 11 +++ betree/src/allocator/worst_fit_simple.rs | 106 +++++++++++++++++++++++ betree/src/database/handler.rs | 1 + 3 files changed, 118 insertions(+) create mode 100644 betree/src/allocator/worst_fit_simple.rs diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 793707fec..f18c43d1f 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -12,6 +12,9 @@ pub use self::next_fit::NextFit; mod best_fit_simple; pub use self::best_fit_simple::BestFitSimple; +mod worst_fit_simple; +pub use self::worst_fit_simple::WorstFitSimple; + mod segment_allocator; pub use self::segment_allocator::SegmentAllocator; @@ -43,6 +46,12 @@ pub enum AllocatorType { /// version uses a linear search to find the best fit. BestFitSimple, + /// **Worst Fit (Simple):** + /// This allocator searches the entire segment and allocates the largest + /// free block. This simple version uses a linear search to find the worst + /// fit. + WorstFitSimple, + /// **Segment Allocator:** /// This is a first fit allocator that was used before making the allocators /// generic. It is not efficient and mainly included for reference. @@ -297,11 +306,13 @@ mod tests { generate_small_tests!(test_first_fit, FirstFit); generate_small_tests!(test_next_fit, NextFit); generate_small_tests!(test_best_fit_simple, BestFitSimple); + generate_small_tests!(test_worst_fit_simple, WorstFitSimple); generate_small_tests!(test_segment_allocator, SegmentAllocator); // Generate fuzz tests for each allocator generate_fuzz_tests!(test_first_fit_fuzz, FirstFit); generate_fuzz_tests!(test_next_fit_fuzz, NextFit); generate_fuzz_tests!(test_best_fit_simple_fuzz, BestFitSimple); + generate_fuzz_tests!(test_worst_fit_simple_fuzz, WorstFitSimple); generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); } diff --git a/betree/src/allocator/worst_fit_simple.rs b/betree/src/allocator/worst_fit_simple.rs new file mode 100644 index 000000000..c063630d9 --- /dev/null +++ b/betree/src/allocator/worst_fit_simple.rs @@ -0,0 +1,106 @@ +use super::*; + +/// Simple worst-fit bitmap allocator +pub struct WorstFitSimple { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), +} + +impl Allocator for WorstFitSimple { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `WorstFitSimple` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + WorstFitSimple { + data: BitArray::new(bitmap), + } + } + + /// Allocates a block of the given `size`. + /// Returns `None` if the allocation request cannot be satisfied. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + let mut worst_fit_offset = None; + // Initialize with size, as we want the largest possible size and it has to be at least + // size large. + let mut worst_fit_size = size; + let mut offset: u32 = 0; + + while offset + size <= SEGMENT_SIZE as u32 { + let end_idx = (offset + size) as usize; + + match self.data[offset as usize..end_idx].last_one() { + Some(last_alloc_idx) => { + // Skip to the end of the last allocated block + offset += last_alloc_idx as u32 + 1; + } + None => { + // Find the next allocated block after the current free range + match self.data[end_idx..].first_one() { + Some(next_alloc_idx) => { + let free_block_size = next_alloc_idx as u32 + end_idx as u32 - offset; + + // Check if this free block is a worse fit (larger) + if free_block_size > worst_fit_size { + worst_fit_offset = Some(offset); + worst_fit_size = free_block_size; + } + + offset = next_alloc_idx as u32 + end_idx as u32 + 1; + } + None => { + // No more allocated blocks, we have scanned the whole segment. + let free_block_size = self.data[offset as usize..].len() as u32; + + // Check if this free block is a worse fit (larger) + if free_block_size > worst_fit_size { + worst_fit_offset = Some(offset); + worst_fit_size = free_block_size; + } + + break; + } + } + } + } + } + + if let Some(offset) = worst_fit_offset { + self.mark(offset, size, Action::Allocate); + } + + worst_fit_offset + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + true + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + } +} diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 4f92e8cde..e0809aa59 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -199,6 +199,7 @@ impl Handler { AllocatorType::FirstFit => Box::new(FirstFit::new(bitmap)), AllocatorType::NextFit => Box::new(NextFit::new(bitmap)), AllocatorType::BestFitSimple => Box::new(BestFitSimple::new(bitmap)), + AllocatorType::WorstFitSimple => Box::new(WorstFitSimple::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From a1389d83cd9ada293576aa1f50a1fd58273651a5 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Thu, 9 Jan 2025 13:49:49 +0100 Subject: [PATCH 06/36] better allocator benchmarks --- betree/Cargo.toml | 1 + betree/benches/allocator.rs | 85 ++++++++++++++++++++++++++++++++++--- betree/src/allocator/mod.rs | 2 +- 3 files changed, 82 insertions(+), 6 deletions(-) diff --git a/betree/Cargo.toml b/betree/Cargo.toml index d9f116a30..ada5cff8b 100644 --- a/betree/Cargo.toml +++ b/betree/Cargo.toml @@ -68,6 +68,7 @@ quickcheck = "1" quickcheck_macros = "1" clap = "2.33" criterion = "0.3" +zipf = "7.0.1" [features] default = ["init_env_logger", "figment_config"] diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index c7dd2c032..090635fee 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,15 +1,90 @@ -use betree_storage_stack::allocator::{SegmentAllocator, SEGMENT_SIZE_BYTES}; +use betree_storage_stack::allocator::{ + Allocator, BestFitSimple, FirstFit, NextFit, SegmentAllocator, WorstFitSimple, + SEGMENT_SIZE_BYTES, +}; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use rand::distributions::{Distribution, Uniform}; +use rand::rngs::StdRng; +use rand::SeedableRng; +use zipf::ZipfDistribution; + +#[derive(Clone)] +enum SizeDistribution { + Uniform(Uniform), + Zipfian(ZipfDistribution), +} + +fn bench_allocator( + b: &mut Bencher, + dist: SizeDistribution, + alloc_ratio: f64, + min_size: usize, + max_size: usize, +) { + let mut allocator = A::new([0; SEGMENT_SIZE_BYTES]); + let mut rng = StdRng::seed_from_u64(42); + let mut allocated = Vec::new(); -fn allocate(b: &mut Bencher) { - let mut a = SegmentAllocator::new([0; SEGMENT_SIZE_BYTES]); b.iter(|| { - black_box(a.allocate(10)); + if rand::random::() < alloc_ratio { + // Allocation path + let size = match &dist { + SizeDistribution::Uniform(u) => u.sample(&mut rng), + SizeDistribution::Zipfian(z) => { + (z.sample(&mut rng) as usize).clamp(min_size, max_size) + } + } as u32; + if let Some(offset) = allocator.allocate(size) { + allocated.push((offset, size)); + } + } else if !allocated.is_empty() { + // Deallocation path + let idx = rand::random::() % allocated.len(); + let (offset, size) = allocated.swap_remove(idx); + allocator.deallocate(offset, size); + } }); } pub fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("allocate", allocate); + let min_size = 64; + let max_size = 4096; + let zipfian_exponent = 0.99; + + let distributions = [ + ( + "uniform", + SizeDistribution::Uniform(Uniform::new(min_size, max_size)), + ), + ( + "zipfian", + SizeDistribution::Zipfian( + ZipfDistribution::new(max_size - min_size, zipfian_exponent).expect(""), + ), + ), + ]; + + let mut group = c.benchmark_group("allocator"); + + for (dist_name, dist) in distributions { + group.bench_function(&format!("first_fit_{}", dist_name), |b| { + bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) + }); + group.bench_function(&format!("next_fit_{}", dist_name), |b| { + bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) + }); + group.bench_function(&format!("best_fit_{}", dist_name), |b| { + bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) + }); + group.bench_function(&format!("worst_fit_{}", dist_name), |b| { + bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) + }); + group.bench_function(&format!("segment_{}", dist_name), |b| { + bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) + }); + } + + group.finish(); } criterion_group!(benches, criterion_benchmark); diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index f18c43d1f..573b2ec03 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -285,7 +285,7 @@ mod tests { allocator.deallocate(offset, 12); } - // 8. Large Allocation Followed by Small Allocations + // Large Allocation Followed by Small Allocations let large_offset = allocator.allocate(100).unwrap(); _ = allocator.allocate(4).unwrap(); _ = allocator.allocate(7).unwrap(); From 6a1c5f60a36c8f1458002a033ba5c90c980e6edc Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Wed, 15 Jan 2025 07:09:54 +0100 Subject: [PATCH 07/36] new benchmark grouping for joint plots --- betree/benches/allocator.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 090635fee..3dc256d23 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -64,28 +64,27 @@ pub fn criterion_benchmark(c: &mut Criterion) { ), ]; - let mut group = c.benchmark_group("allocator"); - for (dist_name, dist) in distributions { - group.bench_function(&format!("first_fit_{}", dist_name), |b| { + let mut group = c.benchmark_group(dist_name); + group.bench_function("first_fit", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); - group.bench_function(&format!("next_fit_{}", dist_name), |b| { + group.bench_function("next_fit", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); - group.bench_function(&format!("best_fit_{}", dist_name), |b| { + group.bench_function("best_fit", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); - group.bench_function(&format!("worst_fit_{}", dist_name), |b| { + group.bench_function("worst_fit", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); - group.bench_function(&format!("segment_{}", dist_name), |b| { + group.bench_function("segment", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); + group.finish(); } - - group.finish(); } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); + From ecbff0ca884efc1f78e03067fd54a6e5627e49c2 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Wed, 15 Jan 2025 12:33:45 +0100 Subject: [PATCH 08/36] remove unused default --- betree/src/data_management/dmu.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/betree/src/data_management/dmu.rs b/betree/src/data_management/dmu.rs index 023034b84..5f2ddbec7 100644 --- a/betree/src/data_management/dmu.rs +++ b/betree/src/data_management/dmu.rs @@ -6,7 +6,7 @@ use super::{ CopyOnWriteEvent, Dml, HasStoragePreference, Object, ObjectReference, }; use crate::{ - allocator::{Action, Allocator, SegmentAllocator, SegmentId, SEGMENT_SIZE}, + allocator::{Action, SegmentId, SEGMENT_SIZE}, buffer::Buf, cache::{Cache, ChangeKeyError, RemoveError}, checksum::{Builder, Checksum, State}, @@ -40,8 +40,6 @@ use std::{ thread::yield_now, }; -type DefaultAllocator = SegmentAllocator; - /// The Data Management Unit. pub struct Dmu where From d383626740736b7f9dcd6527bf5e24c64bd60b31 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 08:26:40 +0100 Subject: [PATCH 09/36] first fit with list building on creation --- betree/src/allocator/first_fit_list.rs | 169 +++++++++++++++++++++++++ betree/src/allocator/mod.rs | 3 + 2 files changed, 172 insertions(+) create mode 100644 betree/src/allocator/first_fit_list.rs diff --git a/betree/src/allocator/first_fit_list.rs b/betree/src/allocator/first_fit_list.rs new file mode 100644 index 000000000..fa2b4c6d9 --- /dev/null +++ b/betree/src/allocator/first_fit_list.rs @@ -0,0 +1,169 @@ +use super::*; + +/// Simple first-fit bitmap allocator that uses a list to manage free segments +pub struct FirstFitList { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + free_segments: Vec<(u32, u32)>, // (offset, size) of free segments +} + +impl Allocator for FirstFitList { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `ListFirstFit` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = FirstFitList { + data, + free_segments: Vec::new(), + }; + allocator.initialize_free_segments(); + allocator + } + + /// Allocates a block of the given `size`. + /// Returns `None` if the allocation request cannot be satisfied and the offset if if can. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + for i in 0..self.free_segments.len() { + let (offset, segment_size) = self.free_segments[i]; + if segment_size >= size { + let alloc_offset = offset; + self.mark(alloc_offset, size, Action::Allocate); + + if segment_size == size { + // perfect fit, remove the segment + // TODO: use swap remove + self.free_segments.remove(i); + } else { + // update the free segment with the remaining size and new offset + self.free_segments[i].0 = alloc_offset + size; + self.free_segments[i].1 = segment_size - size; + } + return Some(alloc_offset); + } + } + None + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + + // Update free_segments to reflect the allocation + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + if seg_offset == offset && seg_size == size { + // perfect fit, remove the segment + self.free_segments.remove(i); + self.mark(offset, size, Action::Allocate); + return true; + } else if seg_offset == offset && seg_size > size { + // allocation at the beginning of the segment, adjust offset and size + self.free_segments[i].0 += size; + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset && offset + size == seg_offset + seg_size { + // allocation at the end of the segment, just adjust size + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset + && offset < seg_offset + seg_size + && offset + size < seg_offset + seg_size + { + // allocation in the middle of the segment, split segment + let remaining_size = seg_size - (size + (offset - seg_offset)); + let new_offset = offset + size; + self.free_segments[i].1 = offset - seg_offset; // adjust current segment size + + self.free_segments + .insert(i + 1, (new_offset, remaining_size)); // insert new segment after current + self.mark(offset, size, Action::Allocate); + return true; + } + } + + false // No suitable free segment found in free_segments list + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + + let dealloc_end = offset + size; + let new_segment = (offset, size); + let mut insert_index = self.free_segments.len(); // Default to append + + // Find the correct insertion index to keep free_segments sorted and check for merging + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + let seg_end = seg_offset + seg_size; + + if seg_end == offset { + // Merge with the preceding segment + self.free_segments[i].1 += size; + // Check if we also need to merge with the following segment + if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { + self.free_segments[i].1 += self.free_segments[i + 1].1; + // TODO: use swap remove + self.free_segments.remove(i + 1); // Remove the merged following segment + } + return; + } else if dealloc_end == seg_offset { + // Merge with the following segment + self.free_segments[i].0 = offset; + self.free_segments[i].1 += size; + return; + } else if seg_offset > offset { + // Insertion point found (segments are sorted by offset) + insert_index = i; + break; + } + } + // If no merge is possible, insert the new segment at the determined index + self.free_segments.insert(insert_index, new_segment); + } +} + +impl FirstFitList { + /// Initializes the `free_segments` vector by scanning the bitmap. + fn initialize_free_segments(&mut self) { + let mut offset: u32 = 0; + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + // If bit is 0, it's free + let start_offset = offset; + let mut current_size = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + self.free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + } +} diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 573b2ec03..002236ea1 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -18,6 +18,9 @@ pub use self::worst_fit_simple::WorstFitSimple; mod segment_allocator; pub use self::segment_allocator::SegmentAllocator; +mod first_fit_list; +pub use self::first_fit_list::FirstFitList; + /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap From e3dee21c01cf4ab846026685590d5fcbd86aad12 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 08:29:28 +0100 Subject: [PATCH 10/36] next fit with list building on creation --- betree/src/allocator/mod.rs | 3 + betree/src/allocator/next_fit_list.rs | 181 ++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 betree/src/allocator/next_fit_list.rs diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 002236ea1..e888f0bed 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -21,6 +21,9 @@ pub use self::segment_allocator::SegmentAllocator; mod first_fit_list; pub use self::first_fit_list::FirstFitList; +mod next_fit_list; +pub use self::next_fit_list::NextFitList; + /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap diff --git a/betree/src/allocator/next_fit_list.rs b/betree/src/allocator/next_fit_list.rs new file mode 100644 index 000000000..9e0322c83 --- /dev/null +++ b/betree/src/allocator/next_fit_list.rs @@ -0,0 +1,181 @@ +use super::*; + +/// Simple Next-Fit bitmap allocator that uses a list to manage free segments +pub struct NextFitList { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + free_segments: Vec<(u32, u32)>, // (offset, size) of free segments + last_offset_index: usize, // Index of the last checked segment in free_segments +} + +impl Allocator for NextFitList { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `ListNextFit` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = NextFitList { + data, + free_segments: Vec::new(), + last_offset_index: 0, // Initialize last_offset_index to 0 + }; + allocator.initialize_free_segments(); + allocator + } + + /// Allocates a block of the given `size`. + /// Returns `None` if the allocation request cannot be satisfied. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + for _ in 0..self.free_segments.len() { + if self.last_offset_index >= self.free_segments.len() { + // Check for wrap-around + self.last_offset_index = 0; + } + + let index = self.last_offset_index; + let (offset, segment_size) = self.free_segments[index]; + + self.last_offset_index += 1; + + if segment_size >= size { + let alloc_offset = offset; + self.mark(alloc_offset, size, Action::Allocate); + + if segment_size == size { + // perfect fit, remove the segment + self.free_segments.remove(index); + if self.free_segments.is_empty() { + self.last_offset_index = 0; // reset last_offset_index if free_segments becomes empty + } + } else { + // update the free segment with the remaining size and new offset + self.free_segments[index].0 = alloc_offset + size; + self.free_segments[index].1 = segment_size - size; + } + return Some(alloc_offset); + } + } + None + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + + // Update free_segments to reflect the allocation + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + if seg_offset == offset && seg_size == size { + // perfect fit, remove the segment + self.free_segments.remove(i); + self.mark(offset, size, Action::Allocate); + return true; + } else if seg_offset == offset && seg_size > size { + // allocation at the beginning of the segment, adjust offset and size + self.free_segments[i].0 += size; + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset && offset + size == seg_offset + seg_size { + // allocation at the end of the segment, just adjust size + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset + && offset < seg_offset + seg_size + && offset + size < seg_offset + seg_size + { + // allocation in the middle of the segment, split segment + let remaining_size = seg_size - (size + (offset - seg_offset)); + let new_offset = offset + size; + self.free_segments[i].1 = offset - seg_offset; // adjust current segment size + + self.free_segments + .insert(i + 1, (new_offset, remaining_size)); // insert new segment after current + self.mark(offset, size, Action::Allocate); + return true; + } + } + + false + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + + let dealloc_end = offset + size; + let new_segment = (offset, size); + let mut insert_index = self.free_segments.len(); + + // Find the correct insertion index to keep free_segments sorted and check for merging + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + let seg_end = seg_offset + seg_size; + + if seg_end == offset { + // Merge with the preceding segment + self.free_segments[i].1 += size; + // Check if we can also merge with the following segment + if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { + self.free_segments[i].1 += self.free_segments[i + 1].1; + self.free_segments.remove(i + 1); // Remove the merged following segment + } + return; + } else if dealloc_end == seg_offset { + // Merge with the following segment + self.free_segments[i].0 = offset; + self.free_segments[i].1 += size; + return; + } else if seg_offset > offset { + // Insertion point found (segments are sorted by offset) + insert_index = i; + break; + } + } + // If no merge is possible, insert the new segment at the determined index + self.free_segments.insert(insert_index, new_segment); + } +} + +impl NextFitList { + /// Initializes the `free_segments` vector by scanning the bitmap. + fn initialize_free_segments(&mut self) { + let mut offset: u32 = 0; + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + // If bit is 0, it's free + let start_offset = offset; + let mut current_size = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + self.free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + } +} From 685538678781b415198215cd5519acf24e18a533 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 10:35:06 +0100 Subject: [PATCH 11/36] benchmark with sync simulation --- betree/benches/allocator.rs | 176 +++++++++++++++++++++++++++++++++--- 1 file changed, 163 insertions(+), 13 deletions(-) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 3dc256d23..1cf8abced 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,6 +1,6 @@ use betree_storage_stack::allocator::{ - Allocator, BestFitSimple, FirstFit, NextFit, SegmentAllocator, WorstFitSimple, - SEGMENT_SIZE_BYTES, + self, Allocator, BestFitSimple, FirstFit, FirstFitList, NextFit, NextFitList, SegmentAllocator, + WorstFitSimple, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::distributions::{Distribution, Uniform}; @@ -22,30 +22,89 @@ fn bench_allocator( max_size: usize, ) { let mut allocator = A::new([0; SEGMENT_SIZE_BYTES]); - let mut rng = StdRng::seed_from_u64(42); let mut allocated = Vec::new(); + let mut rng = StdRng::seed_from_u64(42); + let mut sample_size = || -> u32 { + match &dist { + SizeDistribution::Uniform(u) => return black_box(u.sample(&mut rng)) as u32, + SizeDistribution::Zipfian(z) => { + return (black_box(z.sample(&mut rng)) as usize).clamp(min_size, max_size) as u32 + } + } + }; + b.iter(|| { if rand::random::() < alloc_ratio { // Allocation path - let size = match &dist { - SizeDistribution::Uniform(u) => u.sample(&mut rng), - SizeDistribution::Zipfian(z) => { - (z.sample(&mut rng) as usize).clamp(min_size, max_size) - } - } as u32; - if let Some(offset) = allocator.allocate(size) { + let size = sample_size(); + if let Some(offset) = black_box(allocator.allocate(size)) { allocated.push((offset, size)); } } else if !allocated.is_empty() { // Deallocation path let idx = rand::random::() % allocated.len(); let (offset, size) = allocated.swap_remove(idx); - allocator.deallocate(offset, size); + black_box(allocator.deallocate(offset, size)); } }); } +// In Haura, allocators are not continuously active in memory. Instead, they are loaded from disk +// when needed. This benchmark simulates this behavior by creating a new allocator instance for each +// iteration. Also deallocations are buffered and applied during sync operations, not immediately to +// the allocator. Here, we simulate the sync operation by directly modifying the underlying bitmap +// data after the allocator has performed allocations, mimicking the delayed deallocation process. +fn bench_allocator_with_sync( + b: &mut Bencher, + dist: SizeDistribution, + allocations: u64, + deallocations: u64, + min_size: usize, + max_size: usize, +) { + let data = [0; SEGMENT_SIZE_BYTES]; + let mut allocated = Vec::new(); + + let mut rng = StdRng::seed_from_u64(42); + let mut sample_size = || -> u32 { + match &dist { + SizeDistribution::Uniform(u) => return black_box(u.sample(&mut rng)) as u32, + SizeDistribution::Zipfian(z) => { + return (black_box(z.sample(&mut rng)) as usize).clamp(min_size, max_size) as u32 + } + } + }; + + b.iter(|| { + let mut allocator = A::new(data); + for _ in 0..allocations { + let size = sample_size(); + if let Some(offset) = black_box(allocator.allocate(size)) { + allocated.push((offset, size)); + } + } + + // Simulates the deferred deallocations + let bitmap = allocator.data(); + for _ in 0..deallocations { + if allocated.is_empty() { + break; + } + let idx = rand::random::() % allocated.len(); + let (offset, size) = allocated.swap_remove(idx); + + let start = offset as usize; + let end = (offset + size) as usize; + let range = &mut bitmap[start..end]; + range.fill(false); + } + // At the end of the iteration, the allocator goes out of scope, simulating it being + // unloaded from memory. In the next iteration, a new allocator will be created and loaded + // with the modified bitmap data. + }); +} + pub fn criterion_benchmark(c: &mut Criterion) { let min_size = 64; let max_size = 4096; @@ -64,14 +123,20 @@ pub fn criterion_benchmark(c: &mut Criterion) { ), ]; - for (dist_name, dist) in distributions { + for (dist_name, dist) in distributions.clone() { let mut group = c.benchmark_group(dist_name); group.bench_function("first_fit", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); + group.bench_function("first_fit_list", |b| { + bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) + }); group.bench_function("next_fit", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); + group.bench_function("next_fit_list", |b| { + bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) + }); group.bench_function("best_fit", |b| { bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) }); @@ -83,8 +148,93 @@ pub fn criterion_benchmark(c: &mut Criterion) { }); group.finish(); } + + let alloc_dealloc_ratios = [ + (100, 50), + (500, 250), + (1000, 500), + (5000, 2500), + (10000, 5000), + ]; + + for (dist_name, dist) in distributions { + for (allocations, deallocations) in alloc_dealloc_ratios { + let group_name = format!("{}_sync_{}_{}", dist_name, allocations, deallocations); + let mut group = c.benchmark_group(group_name); + group.bench_function("first_fit", |b| { + bench_allocator_with_sync::( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + group.bench_function("first_fit_list", |b| { + bench_allocator_with_sync::( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + group.bench_function("next_fit", |b| { + bench_allocator_with_sync::( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + group.bench_function("next_fit_list", |b| { + bench_allocator_with_sync::( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + group.bench_function("best_fit", |b| { + bench_allocator_with_sync::( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + group.bench_function("worst_fit", |b| { + bench_allocator_with_sync::( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + group.bench_function("segment", |b| { + bench_allocator_with_sync::( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + group.finish(); + } + } } criterion_group!(benches, criterion_benchmark); criterion_main!(benches); - From 971c28d56686354670170fc53489810988b75d18 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 11:02:12 +0100 Subject: [PATCH 12/36] {first,next}_fit_list optimization --- betree/src/allocator/first_fit_list.rs | 22 ++++++++--------- betree/src/allocator/mod.rs | 16 +++++++++++++ betree/src/allocator/next_fit_list.rs | 33 +++++++++++--------------- betree/src/database/handler.rs | 2 ++ 4 files changed, 42 insertions(+), 31 deletions(-) diff --git a/betree/src/allocator/first_fit_list.rs b/betree/src/allocator/first_fit_list.rs index fa2b4c6d9..6fa7b93a7 100644 --- a/betree/src/allocator/first_fit_list.rs +++ b/betree/src/allocator/first_fit_list.rs @@ -32,20 +32,18 @@ impl Allocator for FirstFitList { for i in 0..self.free_segments.len() { let (offset, segment_size) = self.free_segments[i]; + if segment_size >= size { - let alloc_offset = offset; - self.mark(alloc_offset, size, Action::Allocate); + self.mark(offset, size, Action::Allocate); - if segment_size == size { - // perfect fit, remove the segment - // TODO: use swap remove - self.free_segments.remove(i); - } else { - // update the free segment with the remaining size and new offset - self.free_segments[i].0 = alloc_offset + size; - self.free_segments[i].1 = segment_size - size; - } - return Some(alloc_offset); + // update the free segment with the remaining size and new offset + self.free_segments[i].0 = offset + size; + self.free_segments[i].1 = segment_size - size; + // NOTE: We do not handle the == case here. We could remove that entry from the + // list but we then would need to copy some things because the allocate_at (and + // deallocation) logic depends on a sorted list and also need have extra handling. + // The empty slots get garbage collected on the next sync anyway. + return Some(offset); } } None diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index e888f0bed..335b72105 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -41,11 +41,23 @@ pub enum AllocatorType { /// first free block that is large enough to satisfy the request. FirstFit, + /// **First Fit List:** + /// This allocator builds an internal list of the free space in the segment + /// and then searches from the beginning in that list and allocates the + /// first free block that is large enough to satisfy the request. + FirstFitList, + /// **Next Fit:** /// This allocator starts searching from the last allocation and continues /// searching the segment for the next free block that is large enough. NextFit, + /// **Next Fit List:** + /// This allocator builds an internal list of the free space in the segment + /// and then starts from the last allocation in that list and allocates the + /// next free block that is large enough to satisfy the request. + NextFitList, + /// **Best Fit (Simple):** /// This allocator searches the entire segment and allocates the smallest /// free block that is large enough to satisfy the request. This simple @@ -310,14 +322,18 @@ mod tests { // Generate tests for each allocator generate_small_tests!(test_first_fit, FirstFit); + generate_small_tests!(test_first_fit_list, FirstFitList); generate_small_tests!(test_next_fit, NextFit); + generate_small_tests!(test_next_fit_list, NextFitList); generate_small_tests!(test_best_fit_simple, BestFitSimple); generate_small_tests!(test_worst_fit_simple, WorstFitSimple); generate_small_tests!(test_segment_allocator, SegmentAllocator); // Generate fuzz tests for each allocator generate_fuzz_tests!(test_first_fit_fuzz, FirstFit); + generate_fuzz_tests!(test_first_fit_list_fuzz, FirstFitList); generate_fuzz_tests!(test_next_fit_fuzz, NextFit); + generate_fuzz_tests!(test_next_fit_list_fuzz, NextFitList); generate_fuzz_tests!(test_best_fit_simple_fuzz, BestFitSimple); generate_fuzz_tests!(test_worst_fit_simple_fuzz, WorstFitSimple); generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); diff --git a/betree/src/allocator/next_fit_list.rs b/betree/src/allocator/next_fit_list.rs index 9e0322c83..7c9fe4b69 100644 --- a/betree/src/allocator/next_fit_list.rs +++ b/betree/src/allocator/next_fit_list.rs @@ -38,28 +38,23 @@ impl Allocator for NextFitList { self.last_offset_index = 0; } - let index = self.last_offset_index; - let (offset, segment_size) = self.free_segments[index]; - - self.last_offset_index += 1; + let (offset, segment_size) = self.free_segments[self.last_offset_index]; if segment_size >= size { - let alloc_offset = offset; - self.mark(alloc_offset, size, Action::Allocate); - - if segment_size == size { - // perfect fit, remove the segment - self.free_segments.remove(index); - if self.free_segments.is_empty() { - self.last_offset_index = 0; // reset last_offset_index if free_segments becomes empty - } - } else { - // update the free segment with the remaining size and new offset - self.free_segments[index].0 = alloc_offset + size; - self.free_segments[index].1 = segment_size - size; - } - return Some(alloc_offset); + self.mark(offset, size, Action::Allocate); + + // update the free segment with the remaining size and new offset + self.free_segments[self.last_offset_index].0 = offset + size; + self.free_segments[self.last_offset_index].1 = segment_size - size; + // NOTE: We do not handle the == case here. We could remove that entry from the + // list but we then would need to copy some things because the allocate_at (and + // deallocation) logic depends on a sorted list and also need have extra handling. + // The empty slots get garbage collected on the next sync anyway. + + self.last_offset_index += 1; + return Some(offset); } + self.last_offset_index += 1; } None } diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index e0809aa59..6560e2c51 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -197,7 +197,9 @@ impl Handler { let mut allocator: Box = match self.allocator { AllocatorType::FirstFit => Box::new(FirstFit::new(bitmap)), + AllocatorType::FirstFitList => Box::new(FirstFitList::new(bitmap)), AllocatorType::NextFit => Box::new(NextFit::new(bitmap)), + AllocatorType::NextFitList => Box::new(NextFitList::new(bitmap)), AllocatorType::BestFitSimple => Box::new(BestFitSimple::new(bitmap)), AllocatorType::WorstFitSimple => Box::new(WorstFitSimple::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), From 2876ba81b451f25c6807f2f8e93ba0a60aac38e0 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 12:07:46 +0100 Subject: [PATCH 13/36] {best,worst}_fit_list, renamed bitmap scan allocators --- betree/src/allocator/best_fit_list.rs | 163 ++++++++++++++++++ .../{best_fit_simple.rs => best_fit_scan.rs} | 6 +- .../{first_fit.rs => first_fit_scan.rs} | 6 +- betree/src/allocator/mod.rs | 68 +++++--- .../{next_fit.rs => next_fit_scan.rs} | 6 +- betree/src/allocator/worst_fit_list.rs | 163 ++++++++++++++++++ ...{worst_fit_simple.rs => worst_fit_scan.rs} | 6 +- betree/src/database/handler.rs | 10 +- 8 files changed, 388 insertions(+), 40 deletions(-) create mode 100644 betree/src/allocator/best_fit_list.rs rename betree/src/allocator/{best_fit_simple.rs => best_fit_scan.rs} (97%) rename betree/src/allocator/{first_fit.rs => first_fit_scan.rs} (96%) rename betree/src/allocator/{next_fit.rs => next_fit_scan.rs} (97%) create mode 100644 betree/src/allocator/worst_fit_list.rs rename betree/src/allocator/{worst_fit_simple.rs => worst_fit_scan.rs} (97%) diff --git a/betree/src/allocator/best_fit_list.rs b/betree/src/allocator/best_fit_list.rs new file mode 100644 index 000000000..bc0c5ede5 --- /dev/null +++ b/betree/src/allocator/best_fit_list.rs @@ -0,0 +1,163 @@ +use super::*; + +/// Simple Best-Fit bitmap allocator that uses a list to manage free segments +pub struct BestFitList { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + free_segments: Vec<(u32, u32)>, // (offset, size) of free segments +} + +impl Allocator for BestFitList { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `BestFitList` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = BestFitList { + data, + free_segments: Vec::new(), + }; + allocator.initialize_free_segments(); + allocator + } + + /// Allocates a block of the given `size` using best-fit strategy. + /// Returns `None` if the allocation request cannot be satisfied. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + let mut best_fit_segment_index: Option = None; + let mut best_fit_segment_size: u32 = u32::MAX; // Initialize with a large value + + for i in 0..self.free_segments.len() { + let (_, segment_size) = self.free_segments[i]; + if segment_size >= size && segment_size < best_fit_segment_size { + best_fit_segment_index = Some(i); + best_fit_segment_size = segment_size; + } + } + + if let Some(index) = best_fit_segment_index { + let (offset, segment_size) = self.free_segments[index]; + self.mark(offset, size, Action::Allocate); + + self.free_segments[index].0 = offset + size; + self.free_segments[index].1 = segment_size - size; + + return Some(offset); + } + None + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + + // Update free_segments to reflect the allocation - similar to FirstFitList::allocate_at + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + if seg_offset == offset && seg_size == size { + self.free_segments.remove(i); + self.mark(offset, size, Action::Allocate); + return true; + } else if seg_offset == offset && seg_size > size { + self.free_segments[i].0 += size; + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset && offset + size == seg_offset + seg_size { + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset + && offset < seg_offset + seg_size + && offset + size < seg_offset + seg_size + { + let remaining_size = seg_size - (size + (offset - seg_offset)); + let new_offset = offset + size; + self.free_segments[i].1 = offset - seg_offset; + + self.free_segments + .insert(i + 1, (new_offset, remaining_size)); + self.mark(offset, size, Action::Allocate); + return true; + } + } + + false + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + + let dealloc_end = offset + size; + let new_segment = (offset, size); + let mut insert_index = self.free_segments.len(); + + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + let seg_end = seg_offset + seg_size; + + if seg_end == offset { + // Merge with the preceding segment + self.free_segments[i].1 += size; + if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { + self.free_segments[i].1 += self.free_segments[i + 1].1; + self.free_segments.remove(i + 1); + } + return; + } else if dealloc_end == seg_offset { + // Merge with the following segment + self.free_segments[i].0 = offset; + self.free_segments[i].1 += size; + return; + } else if seg_offset > offset { + insert_index = i; + break; + } + } + self.free_segments.insert(insert_index, new_segment); + } +} + +impl BestFitList { + /// Initializes the `free_segments` vector by scanning the bitmap. + fn initialize_free_segments(&mut self) { + let mut offset: u32 = 0; + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + let start_offset = offset; + let mut current_size = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + self.free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + // keep segments sorted by offset + self.free_segments.sort_by_key(|seg| seg.0); + } +} diff --git a/betree/src/allocator/best_fit_simple.rs b/betree/src/allocator/best_fit_scan.rs similarity index 97% rename from betree/src/allocator/best_fit_simple.rs rename to betree/src/allocator/best_fit_scan.rs index 918bf07e8..52880e65b 100644 --- a/betree/src/allocator/best_fit_simple.rs +++ b/betree/src/allocator/best_fit_scan.rs @@ -1,11 +1,11 @@ use super::*; /// Simple best-fit bitmap allocator -pub struct BestFitSimple { +pub struct BestFitScan { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), } -impl Allocator for BestFitSimple { +impl Allocator for BestFitScan { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -13,7 +13,7 @@ impl Allocator for BestFitSimple { /// Constructs a new `BestFitSimple` given the segment allocation bitmap. /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { - BestFitSimple { + BestFitScan { data: BitArray::new(bitmap), } } diff --git a/betree/src/allocator/first_fit.rs b/betree/src/allocator/first_fit_scan.rs similarity index 96% rename from betree/src/allocator/first_fit.rs rename to betree/src/allocator/first_fit_scan.rs index 745147052..530be1413 100644 --- a/betree/src/allocator/first_fit.rs +++ b/betree/src/allocator/first_fit_scan.rs @@ -1,11 +1,11 @@ use super::*; /// Simple first-fit bitmap allocator -pub struct FirstFit { +pub struct FirstFitScan { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), } -impl Allocator for FirstFit { +impl Allocator for FirstFitScan { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -13,7 +13,7 @@ impl Allocator for FirstFit { /// Constructs a new `FirstFit` given the segment allocation bitmap. /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { - FirstFit { + FirstFitScan { data: BitArray::new(bitmap), } } diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 335b72105..37cef6ac9 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -3,17 +3,17 @@ use bitvec::prelude::*; use byteorder::{BigEndian, ByteOrder}; use serde::{Deserialize, Serialize}; -mod first_fit; -pub use self::first_fit::FirstFit; +mod first_fit_scan; +pub use self::first_fit_scan::FirstFitScan; -mod next_fit; -pub use self::next_fit::NextFit; +mod next_fit_scan; +pub use self::next_fit_scan::NextFitScan; -mod best_fit_simple; -pub use self::best_fit_simple::BestFitSimple; +mod best_fit_scan; +pub use self::best_fit_scan::BestFitScan; -mod worst_fit_simple; -pub use self::worst_fit_simple::WorstFitSimple; +mod worst_fit_scan; +pub use self::worst_fit_scan::WorstFitScan; mod segment_allocator; pub use self::segment_allocator::SegmentAllocator; @@ -24,6 +24,12 @@ pub use self::first_fit_list::FirstFitList; mod next_fit_list; pub use self::next_fit_list::NextFitList; +mod best_fit_list; +pub use self::best_fit_list::BestFitList; + +mod worst_fit_list; +pub use self::worst_fit_list::WorstFitList; + /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap @@ -36,10 +42,10 @@ const SEGMENT_SIZE_MASK: usize = SEGMENT_SIZE - 1; #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[serde(rename_all = "lowercase")] // This will deserialize "firstfit" as FirstFit pub enum AllocatorType { - /// **First Fit:** + /// **First Fit Scan:** /// This allocator searches the segment from the beginning and allocates the /// first free block that is large enough to satisfy the request. - FirstFit, + FirstFitScan, /// **First Fit List:** /// This allocator builds an internal list of the free space in the segment @@ -47,10 +53,10 @@ pub enum AllocatorType { /// first free block that is large enough to satisfy the request. FirstFitList, - /// **Next Fit:** + /// **Next Fit Scan:** /// This allocator starts searching from the last allocation and continues /// searching the segment for the next free block that is large enough. - NextFit, + NextFitScan, /// **Next Fit List:** /// This allocator builds an internal list of the free space in the segment @@ -58,17 +64,27 @@ pub enum AllocatorType { /// next free block that is large enough to satisfy the request. NextFitList, - /// **Best Fit (Simple):** + /// **Best Fit Scan:** /// This allocator searches the entire segment and allocates the smallest /// free block that is large enough to satisfy the request. This simple /// version uses a linear search to find the best fit. - BestFitSimple, + BestFitScan, - /// **Worst Fit (Simple):** + /// **Best Fit List:** + /// This allocator maintains a list of free segments and chooses the best-fit + /// segment from this list to allocate memory. + BestFitList, + + /// **Worst Fit Scan:** /// This allocator searches the entire segment and allocates the largest /// free block. This simple version uses a linear search to find the worst /// fit. - WorstFitSimple, + WorstFitScan, + + /// **Worst Fit List:** + /// This allocator maintains a list of free segments and chooses the worst-fit + /// (largest) segment from this list to allocate memory. + WorstFitList, /// **Segment Allocator:** /// This is a first fit allocator that was used before making the allocators @@ -321,20 +337,24 @@ mod tests { } // Generate tests for each allocator - generate_small_tests!(test_first_fit, FirstFit); + generate_small_tests!(test_first_fit_scan, FirstFitScan); generate_small_tests!(test_first_fit_list, FirstFitList); - generate_small_tests!(test_next_fit, NextFit); + generate_small_tests!(test_next_fit_scan, NextFitScan); generate_small_tests!(test_next_fit_list, NextFitList); - generate_small_tests!(test_best_fit_simple, BestFitSimple); - generate_small_tests!(test_worst_fit_simple, WorstFitSimple); + generate_small_tests!(test_best_fit_scan, BestFitScan); + generate_small_tests!(test_best_fit_list, BestFitList); + generate_small_tests!(test_worst_fit_scan, WorstFitScan); + generate_small_tests!(test_worst_fit_list, WorstFitList); generate_small_tests!(test_segment_allocator, SegmentAllocator); // Generate fuzz tests for each allocator - generate_fuzz_tests!(test_first_fit_fuzz, FirstFit); + generate_fuzz_tests!(test_first_fit_scan_fuzz, FirstFitScan); generate_fuzz_tests!(test_first_fit_list_fuzz, FirstFitList); - generate_fuzz_tests!(test_next_fit_fuzz, NextFit); + generate_fuzz_tests!(test_next_fit_scan_fuzz, NextFitScan); generate_fuzz_tests!(test_next_fit_list_fuzz, NextFitList); - generate_fuzz_tests!(test_best_fit_simple_fuzz, BestFitSimple); - generate_fuzz_tests!(test_worst_fit_simple_fuzz, WorstFitSimple); + generate_fuzz_tests!(test_best_fit_scan_fuzz, BestFitScan); + generate_fuzz_tests!(test_best_fit_list_fuzz, BestFitList); + generate_fuzz_tests!(test_worst_fit_scan_fuzz, WorstFitScan); + generate_fuzz_tests!(test_worst_fit_list_fuzz, WorstFitList); generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); } diff --git a/betree/src/allocator/next_fit.rs b/betree/src/allocator/next_fit_scan.rs similarity index 97% rename from betree/src/allocator/next_fit.rs rename to betree/src/allocator/next_fit_scan.rs index e3fb89800..1441443f5 100644 --- a/betree/src/allocator/next_fit.rs +++ b/betree/src/allocator/next_fit_scan.rs @@ -1,12 +1,12 @@ use super::*; /// Simple next-fit bitmap allocator -pub struct NextFit { +pub struct NextFitScan { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), last_offset: u32, } -impl Allocator for NextFit { +impl Allocator for NextFitScan { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -14,7 +14,7 @@ impl Allocator for NextFit { /// Constructs a new `NextFit` given the segment allocation bitmap. /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { - NextFit { + NextFitScan { data: BitArray::new(bitmap), last_offset: 0, } diff --git a/betree/src/allocator/worst_fit_list.rs b/betree/src/allocator/worst_fit_list.rs new file mode 100644 index 000000000..3ee80eaa4 --- /dev/null +++ b/betree/src/allocator/worst_fit_list.rs @@ -0,0 +1,163 @@ +use super::*; + +/// Simple Worst-Fit bitmap allocator that uses a list to manage free segments +pub struct WorstFitList { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + free_segments: Vec<(u32, u32)>, // (offset, size) of free segments +} + +impl Allocator for WorstFitList { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `WorstFitList` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = WorstFitList { + data, + free_segments: Vec::new(), + }; + allocator.initialize_free_segments(); + allocator + } + + /// Allocates a block of the given `size` using worst-fit strategy. + /// Returns `None` if the allocation request cannot be satisfied. + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + let mut worst_fit_segment_index: Option = None; + let mut worst_fit_segment_size: u32 = 0; // Initialize with a small value + + for i in 0..self.free_segments.len() { + let (_, segment_size) = self.free_segments[i]; + if segment_size >= size && segment_size > worst_fit_segment_size { + worst_fit_segment_index = Some(i); + worst_fit_segment_size = segment_size; + } + } + + if let Some(index) = worst_fit_segment_index { + let (offset, segment_size) = self.free_segments[index]; + self.mark(offset, size, Action::Allocate); + + self.free_segments[index].0 = offset + size; + self.free_segments[index].1 = segment_size - size; + + return Some(offset); + } + None + } + + /// Allocates a block of the given `size` at `offset`. + /// Returns `false` if the allocation request cannot be satisfied. + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + + // Update free_segments to reflect the allocation - similar to FirstFitList::allocate_at + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + if seg_offset == offset && seg_size == size { + self.free_segments.remove(i); + self.mark(offset, size, Action::Allocate); + return true; + } else if seg_offset == offset && seg_size > size { + self.free_segments[i].0 += size; + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset && offset + size == seg_offset + seg_size { + self.free_segments[i].1 -= size; + self.mark(offset, size, Action::Allocate); + return true; + } else if offset > seg_offset + && offset < seg_offset + seg_size + && offset + size < seg_offset + seg_size + { + let remaining_size = seg_size - (size + (offset - seg_offset)); + let new_offset = offset + size; + self.free_segments[i].1 = offset - seg_offset; + + self.free_segments + .insert(i + 1, (new_offset, remaining_size)); + self.mark(offset, size, Action::Allocate); + return true; + } + } + + false + } + + /// Deallocates the allocated block. + fn deallocate(&mut self, offset: u32, size: u32) { + if offset + size > SEGMENT_SIZE as u32 { + return; + } + self.mark(offset, size, Action::Deallocate); + + let dealloc_end = offset + size; + let new_segment = (offset, size); + let mut insert_index = self.free_segments.len(); + + for i in 0..self.free_segments.len() { + let (seg_offset, seg_size) = self.free_segments[i]; + let seg_end = seg_offset + seg_size; + + if seg_end == offset { + // Merge with the preceding segment + self.free_segments[i].1 += size; + if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { + self.free_segments[i].1 += self.free_segments[i + 1].1; + self.free_segments.remove(i + 1); + } + return; + } else if dealloc_end == seg_offset { + // Merge with the following segment + self.free_segments[i].0 = offset; + self.free_segments[i].1 += size; + return; + } else if seg_offset > offset { + insert_index = i; + break; + } + } + self.free_segments.insert(insert_index, new_segment); + } +} + +impl WorstFitList { + /// Initializes the `free_segments` vector by scanning the bitmap. + fn initialize_free_segments(&mut self) { + let mut offset: u32 = 0; + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + let start_offset = offset; + let mut current_size = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + self.free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + // keep segments sorted by offset + self.free_segments.sort_by_key(|seg| seg.0); + } +} diff --git a/betree/src/allocator/worst_fit_simple.rs b/betree/src/allocator/worst_fit_scan.rs similarity index 97% rename from betree/src/allocator/worst_fit_simple.rs rename to betree/src/allocator/worst_fit_scan.rs index c063630d9..12d25cee0 100644 --- a/betree/src/allocator/worst_fit_simple.rs +++ b/betree/src/allocator/worst_fit_scan.rs @@ -1,11 +1,11 @@ use super::*; /// Simple worst-fit bitmap allocator -pub struct WorstFitSimple { +pub struct WorstFitScan { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), } -impl Allocator for WorstFitSimple { +impl Allocator for WorstFitScan { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -13,7 +13,7 @@ impl Allocator for WorstFitSimple { /// Constructs a new `WorstFitSimple` given the segment allocation bitmap. /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { - WorstFitSimple { + WorstFitScan { data: BitArray::new(bitmap), } } diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 6560e2c51..5a38ec399 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -196,12 +196,14 @@ impl Handler { } let mut allocator: Box = match self.allocator { - AllocatorType::FirstFit => Box::new(FirstFit::new(bitmap)), + AllocatorType::FirstFitScan => Box::new(FirstFitScan::new(bitmap)), AllocatorType::FirstFitList => Box::new(FirstFitList::new(bitmap)), - AllocatorType::NextFit => Box::new(NextFit::new(bitmap)), + AllocatorType::NextFitScan => Box::new(NextFitScan::new(bitmap)), AllocatorType::NextFitList => Box::new(NextFitList::new(bitmap)), - AllocatorType::BestFitSimple => Box::new(BestFitSimple::new(bitmap)), - AllocatorType::WorstFitSimple => Box::new(WorstFitSimple::new(bitmap)), + AllocatorType::BestFitScan => Box::new(BestFitScan::new(bitmap)), + AllocatorType::BestFitList => Box::new(BestFitList::new(bitmap)), + AllocatorType::WorstFitScan => Box::new(WorstFitScan::new(bitmap)), + AllocatorType::WorstFitList => Box::new(WorstFitList::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From 02335d8b1d057da2aab2cf19c8240936424774f0 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 12:08:30 +0100 Subject: [PATCH 14/36] generic benchmark implementation for easier adding of allocators --- betree/benches/allocator.rs | 191 ++++++++++++++++++------------------ 1 file changed, 98 insertions(+), 93 deletions(-) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 1cf8abced..8a1dd491a 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,6 +1,6 @@ use betree_storage_stack::allocator::{ - self, Allocator, BestFitSimple, FirstFit, FirstFitList, NextFit, NextFitList, SegmentAllocator, - WorstFitSimple, SEGMENT_SIZE_BYTES, + self, Allocator, BestFitList, BestFitScan, FirstFitList, FirstFitScan, NextFitList, + NextFitScan, SegmentAllocator, WorstFitList, WorstFitScan, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::distributions::{Distribution, Uniform}; @@ -14,6 +14,72 @@ enum SizeDistribution { Zipfian(ZipfDistribution), } +// Define a trait for our benchmark struct to allow for trait objects +trait GenericAllocatorBenchmark { + fn benchmark_name(&self) -> &'static str; + fn bench_allocator( + &self, + b: &mut Bencher, + dist: SizeDistribution, + alloc_ratio: f64, + min_size: usize, + max_size: usize, + ); + + fn bench_allocator_with_sync( + &self, + b: &mut Bencher, + dist: SizeDistribution, + allocations: u64, + deallocations: u64, + min_size: usize, + max_size: usize, + ); +} + +struct AllocatorBenchmark { + allocator_type: std::marker::PhantomData, + benchmark_name: &'static str, +} + +impl AllocatorBenchmark { + fn new(benchmark_name: &'static str) -> Self { + AllocatorBenchmark { + allocator_type: std::marker::PhantomData, + benchmark_name, + } + } +} + +impl GenericAllocatorBenchmark for AllocatorBenchmark { + fn benchmark_name(&self) -> &'static str { + self.benchmark_name + } + + fn bench_allocator( + &self, + b: &mut Bencher, + dist: SizeDistribution, + alloc_ratio: f64, + min_size: usize, + max_size: usize, + ) { + bench_allocator::(b, dist, alloc_ratio, min_size, max_size) + } + + fn bench_allocator_with_sync( + &self, + b: &mut Bencher, + dist: SizeDistribution, + allocations: u64, + deallocations: u64, + min_size: usize, + max_size: usize, + ) { + bench_allocator_with_sync::(b, dist, allocations, deallocations, min_size, max_size) + } +} + fn bench_allocator( b: &mut Bencher, dist: SizeDistribution, @@ -123,29 +189,26 @@ pub fn criterion_benchmark(c: &mut Criterion) { ), ]; + // Define the allocators to benchmark + let allocator_benchmarks: Vec> = vec![ + Box::new(AllocatorBenchmark::::new("first_fit_scan")), + Box::new(AllocatorBenchmark::::new("first_fit_list")), + Box::new(AllocatorBenchmark::::new("next_fit_scan")), + Box::new(AllocatorBenchmark::::new("next_fit_list")), + Box::new(AllocatorBenchmark::::new("best_fit_scan")), + Box::new(AllocatorBenchmark::::new("best_fit_list")), + Box::new(AllocatorBenchmark::::new("worst_fit_scan")), + Box::new(AllocatorBenchmark::::new("worst_fit_list")), + Box::new(AllocatorBenchmark::::new("segment")), + ]; + for (dist_name, dist) in distributions.clone() { let mut group = c.benchmark_group(dist_name); - group.bench_function("first_fit", |b| { - bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) - }); - group.bench_function("first_fit_list", |b| { - bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) - }); - group.bench_function("next_fit", |b| { - bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) - }); - group.bench_function("next_fit_list", |b| { - bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) - }); - group.bench_function("best_fit", |b| { - bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) - }); - group.bench_function("worst_fit", |b| { - bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) - }); - group.bench_function("segment", |b| { - bench_allocator::(b, dist.clone(), 0.8, min_size, max_size) - }); + for allocator_bench in &allocator_benchmarks { + group.bench_function(allocator_bench.benchmark_name(), |b| { + allocator_bench.bench_allocator(b, dist.clone(), 0.8, min_size, max_size) + }); + } group.finish(); } @@ -161,76 +224,18 @@ pub fn criterion_benchmark(c: &mut Criterion) { for (allocations, deallocations) in alloc_dealloc_ratios { let group_name = format!("{}_sync_{}_{}", dist_name, allocations, deallocations); let mut group = c.benchmark_group(group_name); - group.bench_function("first_fit", |b| { - bench_allocator_with_sync::( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); - group.bench_function("first_fit_list", |b| { - bench_allocator_with_sync::( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); - group.bench_function("next_fit", |b| { - bench_allocator_with_sync::( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); - group.bench_function("next_fit_list", |b| { - bench_allocator_with_sync::( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); - group.bench_function("best_fit", |b| { - bench_allocator_with_sync::( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); - group.bench_function("worst_fit", |b| { - bench_allocator_with_sync::( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); - group.bench_function("segment", |b| { - bench_allocator_with_sync::( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); + for allocator_bench in &allocator_benchmarks { + group.bench_function(allocator_bench.benchmark_name(), |b| { + allocator_bench.bench_allocator_with_sync( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + } group.finish(); } } From eb8918c5c99048db043f371fd45a52505b9b2d02 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 17:37:20 +0100 Subject: [PATCH 15/36] first fit fsm allocator --- betree/benches/allocator.rs | 5 +- betree/src/allocator/first_fit_fsm.rs | 306 ++++++++++++++++++++++++++ betree/src/allocator/mod.rs | 10 + betree/src/database/handler.rs | 1 + 4 files changed, 320 insertions(+), 2 deletions(-) create mode 100644 betree/src/allocator/first_fit_fsm.rs diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 8a1dd491a..e9dcf5bc5 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,6 +1,6 @@ use betree_storage_stack::allocator::{ - self, Allocator, BestFitList, BestFitScan, FirstFitList, FirstFitScan, NextFitList, - NextFitScan, SegmentAllocator, WorstFitList, WorstFitScan, SEGMENT_SIZE_BYTES, + self, Allocator, BestFitList, BestFitScan, FirstFitFSM, FirstFitList, FirstFitScan, + NextFitList, NextFitScan, SegmentAllocator, WorstFitList, WorstFitScan, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::distributions::{Distribution, Uniform}; @@ -193,6 +193,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { let allocator_benchmarks: Vec> = vec![ Box::new(AllocatorBenchmark::::new("first_fit_scan")), Box::new(AllocatorBenchmark::::new("first_fit_list")), + Box::new(AllocatorBenchmark::::new("first_fit_fsm")), Box::new(AllocatorBenchmark::::new("next_fit_scan")), Box::new(AllocatorBenchmark::::new("next_fit_list")), Box::new(AllocatorBenchmark::::new("best_fit_scan")), diff --git a/betree/src/allocator/first_fit_fsm.rs b/betree/src/allocator/first_fit_fsm.rs new file mode 100644 index 000000000..005184d5a --- /dev/null +++ b/betree/src/allocator/first_fit_fsm.rs @@ -0,0 +1,306 @@ +use super::*; + +/// Based on the free-space-map allocator from postgresql: +/// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README +pub struct FirstFitFSM { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space + tree_height: u32, +} + +impl Allocator for FirstFitFSM { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `FirstFitFSM` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = FirstFitFSM { + data, + fsm_tree: Vec::new(), + tree_height: 0, + }; + allocator.build_fsm_tree(); + allocator + } + + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + if self.fsm_tree[0].1 < size { + return None; // Not enough free space + } + + let mut current_node_index = 0; + while current_node_index < self.fsm_tree.len() / 2 { + let left_child_index = 2 * current_node_index + 1; + let right_child_index = 2 * current_node_index + 2; + + // Check left child first for first fit + if let Some(left_child_value) = self.fsm_tree.get(left_child_index) { + if left_child_value.1 >= size { + current_node_index = left_child_index; + continue; // Go deeper into left subtree + } + } + if let Some(right_child_value) = self.fsm_tree.get(right_child_index) { + if right_child_value.1 >= size { + current_node_index = right_child_index; + continue; // Go deeper into right subtree + } + } + unreachable!(); + } + + // current_node_index is now the index of the best-fit leaf node + assert!(current_node_index >= self.fsm_tree.len() / 2); + let (offset, segment_size) = self.fsm_tree[current_node_index]; + + assert!(segment_size >= size); + + self.mark(offset, size, Action::Allocate); + + // Update the free segment size in the leaf node + self.fsm_tree[current_node_index].0 += size; + self.fsm_tree[current_node_index].1 -= size; + + // Update internal nodes up to the root + let mut current_index = current_node_index; + while current_index > 0 { + current_index = (current_index - 1) / 2; // Index of parent node + let left_child_index = 2 * current_index + 1; + let right_child_index = 2 * current_index + 2; + + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[current_index] = left_child_value + } else { + self.fsm_tree[current_index] = right_child_value + } + } + + return Some(offset); + } + + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + // Because the tree is sorted by offset because of how it's build, this shouldn't be to + // hard to implement efficiently + todo!() + } + + fn deallocate(&mut self, _offset: u32, _size: u32) { + unimplemented!("Not needed right now"); + } +} + +impl FirstFitFSM { + fn get_free_segments(&mut self) -> Vec<(u32, u32)> { + let mut offset: u32 = 0; + let mut free_segments = Vec::new(); + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + // If bit is 0, it's free + let start_offset = offset; + let mut current_size: u32 = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + free_segments + } + + fn build_fsm_tree(&mut self) { + let leaf_nodes = self.get_free_segments(); + let leaf_nodes_num = leaf_nodes.len(); + + if leaf_nodes_num == 0 { + self.fsm_tree = vec![(0, 0)]; // Root node with 0 free space + return; + } + + // Calculate the size of the FSM tree array. For simplicity we assume complete tree for now. + self.tree_height = (leaf_nodes_num as f64).log2().ceil() as u32; + // Number of nodes in complete binary tree of height h is 2^(h+1) - 1 + let tree_nodes_num = (1 << (self.tree_height + 1)) - 1; + + self.fsm_tree.clear(); + self.fsm_tree.resize(tree_nodes_num as usize, (0, 0)); + + // 1. Initialize leaf nodes in fsm_tree from free_segments + // OPTIM: just use memcpy + for (i, &(offset, size)) in leaf_nodes.iter().enumerate() { + // Leaf nodes are at the end of the fsm_tree array in a complete binary tree + let leaf_index = (tree_nodes_num / 2) + i; + if leaf_index < tree_nodes_num { + // Prevent out-of-bounds access if free_segments.len() is not power of 2 + self.fsm_tree[leaf_index] = (offset, size); + } + } + + // 2. Build internal nodes bottom-up similar to a binary heap + for i in (0..(tree_nodes_num / 2)).rev() { + let left_child_index = 2 * i + 1; + let right_child_index = 2 * i + 2; + + // Default to 0 if index is out of bounds (incomplete tree) + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[i] = left_child_value + } else { + self.fsm_tree[i] = right_child_value + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_empty() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let allocator = FirstFitFSM::new(bitmap); + + // In an empty bitmap, the root node should have a large free space + assert_eq!(allocator.fsm_tree[0].0, 0 as u32); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32); + assert_eq!(allocator.tree_height, 0); + } + + #[test] + fn build_simple() { + // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments + bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning + bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones + + let mut allocator = FirstFitFSM::new(bitmap.into_inner()); + + let fsm_tree = vec![ + (7, SEGMENT_SIZE as u32 - 7), + (3, 2), + (7, SEGMENT_SIZE as u32 - 7), + ]; + assert_eq!(allocator.fsm_tree, fsm_tree); + assert_eq!(allocator.tree_height, 1); + } + + #[test] + fn build_complex() { + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let allocator = FirstFitFSM::new(bitmap.into_inner()); + + // binary heap layout + let fsm_tree = vec![ + (53, SEGMENT_SIZE as u32 - 53), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (10, 4), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (3, 2), + (10, 4), + (22, 13), + (36, 6), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (0, 0), + (0, 0), + ]; + + assert_eq!(fsm_tree, allocator.fsm_tree); + assert_eq!(allocator.tree_height, 3); + } + + #[test] + fn allocate_empty_fsm_tree() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let mut allocator = FirstFitFSM::new(bitmap); + + let allocation = allocator.allocate(1024); + assert!(allocation.is_some()); // Allocation should succeed + + let allocated_offset = allocation.unwrap(); + assert_eq!(allocated_offset, 0); // Should allocate at the beginning + + // Check if the allocated region is marked as used in the bitmap + assert!(allocator.data()[0..1024 as usize].all()); + // Check root node value after allocation + assert_eq!(allocator.fsm_tree[0], (1024, SEGMENT_SIZE as u32 - 1024)); + } + + #[test] + fn allocate_complex_fsm_tree() { + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let mut allocator = FirstFitFSM::new(bitmap.into_inner()); + + // Best-fit should allocate from the segment at offset 3 with size 2 + let allocation = allocator.allocate(2); // Request allocation of size 2 + assert!(allocation.is_some()); + assert_eq!(allocation.unwrap(), 3); + // Verify that the allocated region is marked in the bitmap + assert!(allocator.data()[3..5].all()); + + let allocation2 = allocator.allocate(10); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 22); + assert!(allocator.data()[22..32].all()); + + // Allocate again, to use the next best fit segment + let allocation2 = allocator.allocate(100); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 53); + assert!(allocator.data()[53..153].all()); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32 - 153); + } + + #[test] + fn allocate_fail_fsm_tree() { + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let root_free_space = allocator.fsm_tree[0].1; + + // Try to allocate more than available space + let allocation = allocator.allocate(root_free_space + 1); + assert!(allocation.is_none()); // Allocation should fail + + // Check if fsm_tree root value is still the same + assert_eq!(allocator.fsm_tree[0].1, root_free_space); // Should remain unchanged + } +} diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 37cef6ac9..f3e2b471c 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -30,6 +30,9 @@ pub use self::best_fit_list::BestFitList; mod worst_fit_list; pub use self::worst_fit_list::WorstFitList; +mod first_fit_fsm; +pub use self::first_fit_fsm::FirstFitFSM; + /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap @@ -53,6 +56,11 @@ pub enum AllocatorType { /// first free block that is large enough to satisfy the request. FirstFitList, + /// **First Fit FSM:** + /// This allocator builds a binary tree of offsets and sizes, that has the + /// max-heap property on the sizes and uses it to find suitable free space. + FirstFitFSM, + /// **Next Fit Scan:** /// This allocator starts searching from the last allocation and continues /// searching the segment for the next free block that is large enough. @@ -339,6 +347,7 @@ mod tests { // Generate tests for each allocator generate_small_tests!(test_first_fit_scan, FirstFitScan); generate_small_tests!(test_first_fit_list, FirstFitList); + generate_small_tests!(test_first_fit_fsm, FirstFitFSM); generate_small_tests!(test_next_fit_scan, NextFitScan); generate_small_tests!(test_next_fit_list, NextFitList); generate_small_tests!(test_best_fit_scan, BestFitScan); @@ -350,6 +359,7 @@ mod tests { // Generate fuzz tests for each allocator generate_fuzz_tests!(test_first_fit_scan_fuzz, FirstFitScan); generate_fuzz_tests!(test_first_fit_list_fuzz, FirstFitList); + generate_fuzz_tests!(test_first_fit_fsm_fuzz, FirstFitFSM); generate_fuzz_tests!(test_next_fit_scan_fuzz, NextFitScan); generate_fuzz_tests!(test_next_fit_list_fuzz, NextFitList); generate_fuzz_tests!(test_best_fit_scan_fuzz, BestFitScan); diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 5a38ec399..430b41f28 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -198,6 +198,7 @@ impl Handler { let mut allocator: Box = match self.allocator { AllocatorType::FirstFitScan => Box::new(FirstFitScan::new(bitmap)), AllocatorType::FirstFitList => Box::new(FirstFitList::new(bitmap)), + AllocatorType::FirstFitFSM => Box::new(FirstFitFSM::new(bitmap)), AllocatorType::NextFitScan => Box::new(NextFitScan::new(bitmap)), AllocatorType::NextFitList => Box::new(NextFitList::new(bitmap)), AllocatorType::BestFitScan => Box::new(BestFitScan::new(bitmap)), From aede2de65b7250133f9de6134f6fe80850b34c84 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 17:55:48 +0100 Subject: [PATCH 16/36] remove deallocation from Allocator trait --- betree/benches/allocator.rs | 65 ---------- betree/src/allocator/best_fit_list.rs | 36 ------ betree/src/allocator/best_fit_scan.rs | 8 -- betree/src/allocator/first_fit_fsm.rs | 4 - betree/src/allocator/first_fit_list.rs | 41 ------ betree/src/allocator/first_fit_scan.rs | 8 -- betree/src/allocator/mod.rs | 148 +++------------------- betree/src/allocator/next_fit_list.rs | 40 ------ betree/src/allocator/next_fit_scan.rs | 8 -- betree/src/allocator/segment_allocator.rs | 8 -- betree/src/allocator/worst_fit_list.rs | 36 ------ betree/src/allocator/worst_fit_scan.rs | 8 -- 12 files changed, 17 insertions(+), 393 deletions(-) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index e9dcf5bc5..41d75c240 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -17,14 +17,6 @@ enum SizeDistribution { // Define a trait for our benchmark struct to allow for trait objects trait GenericAllocatorBenchmark { fn benchmark_name(&self) -> &'static str; - fn bench_allocator( - &self, - b: &mut Bencher, - dist: SizeDistribution, - alloc_ratio: f64, - min_size: usize, - max_size: usize, - ); fn bench_allocator_with_sync( &self, @@ -56,17 +48,6 @@ impl GenericAllocatorBenchmark for AllocatorBenchmark self.benchmark_name } - fn bench_allocator( - &self, - b: &mut Bencher, - dist: SizeDistribution, - alloc_ratio: f64, - min_size: usize, - max_size: usize, - ) { - bench_allocator::(b, dist, alloc_ratio, min_size, max_size) - } - fn bench_allocator_with_sync( &self, b: &mut Bencher, @@ -80,42 +61,6 @@ impl GenericAllocatorBenchmark for AllocatorBenchmark } } -fn bench_allocator( - b: &mut Bencher, - dist: SizeDistribution, - alloc_ratio: f64, - min_size: usize, - max_size: usize, -) { - let mut allocator = A::new([0; SEGMENT_SIZE_BYTES]); - let mut allocated = Vec::new(); - - let mut rng = StdRng::seed_from_u64(42); - let mut sample_size = || -> u32 { - match &dist { - SizeDistribution::Uniform(u) => return black_box(u.sample(&mut rng)) as u32, - SizeDistribution::Zipfian(z) => { - return (black_box(z.sample(&mut rng)) as usize).clamp(min_size, max_size) as u32 - } - } - }; - - b.iter(|| { - if rand::random::() < alloc_ratio { - // Allocation path - let size = sample_size(); - if let Some(offset) = black_box(allocator.allocate(size)) { - allocated.push((offset, size)); - } - } else if !allocated.is_empty() { - // Deallocation path - let idx = rand::random::() % allocated.len(); - let (offset, size) = allocated.swap_remove(idx); - black_box(allocator.deallocate(offset, size)); - } - }); -} - // In Haura, allocators are not continuously active in memory. Instead, they are loaded from disk // when needed. This benchmark simulates this behavior by creating a new allocator instance for each // iteration. Also deallocations are buffered and applied during sync operations, not immediately to @@ -203,16 +148,6 @@ pub fn criterion_benchmark(c: &mut Criterion) { Box::new(AllocatorBenchmark::::new("segment")), ]; - for (dist_name, dist) in distributions.clone() { - let mut group = c.benchmark_group(dist_name); - for allocator_bench in &allocator_benchmarks { - group.bench_function(allocator_bench.benchmark_name(), |b| { - allocator_bench.bench_allocator(b, dist.clone(), 0.8, min_size, max_size) - }); - } - group.finish(); - } - let alloc_dealloc_ratios = [ (100, 50), (500, 250), diff --git a/betree/src/allocator/best_fit_list.rs b/betree/src/allocator/best_fit_list.rs index bc0c5ede5..8e7676bdd 100644 --- a/betree/src/allocator/best_fit_list.rs +++ b/betree/src/allocator/best_fit_list.rs @@ -102,42 +102,6 @@ impl Allocator for BestFitList { false } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - - let dealloc_end = offset + size; - let new_segment = (offset, size); - let mut insert_index = self.free_segments.len(); - - for i in 0..self.free_segments.len() { - let (seg_offset, seg_size) = self.free_segments[i]; - let seg_end = seg_offset + seg_size; - - if seg_end == offset { - // Merge with the preceding segment - self.free_segments[i].1 += size; - if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { - self.free_segments[i].1 += self.free_segments[i + 1].1; - self.free_segments.remove(i + 1); - } - return; - } else if dealloc_end == seg_offset { - // Merge with the following segment - self.free_segments[i].0 = offset; - self.free_segments[i].1 += size; - return; - } else if seg_offset > offset { - insert_index = i; - break; - } - } - self.free_segments.insert(insert_index, new_segment); - } } impl BestFitList { diff --git a/betree/src/allocator/best_fit_scan.rs b/betree/src/allocator/best_fit_scan.rs index 52880e65b..264456708 100644 --- a/betree/src/allocator/best_fit_scan.rs +++ b/betree/src/allocator/best_fit_scan.rs @@ -98,12 +98,4 @@ impl Allocator for BestFitScan { self.mark(offset, size, Action::Allocate); true } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - } } diff --git a/betree/src/allocator/first_fit_fsm.rs b/betree/src/allocator/first_fit_fsm.rs index 005184d5a..87fc583d8 100644 --- a/betree/src/allocator/first_fit_fsm.rs +++ b/betree/src/allocator/first_fit_fsm.rs @@ -92,10 +92,6 @@ impl Allocator for FirstFitFSM { // hard to implement efficiently todo!() } - - fn deallocate(&mut self, _offset: u32, _size: u32) { - unimplemented!("Not needed right now"); - } } impl FirstFitFSM { diff --git a/betree/src/allocator/first_fit_list.rs b/betree/src/allocator/first_fit_list.rs index 6fa7b93a7..f7e54c758 100644 --- a/betree/src/allocator/first_fit_list.rs +++ b/betree/src/allocator/first_fit_list.rs @@ -102,47 +102,6 @@ impl Allocator for FirstFitList { false // No suitable free segment found in free_segments list } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - - let dealloc_end = offset + size; - let new_segment = (offset, size); - let mut insert_index = self.free_segments.len(); // Default to append - - // Find the correct insertion index to keep free_segments sorted and check for merging - for i in 0..self.free_segments.len() { - let (seg_offset, seg_size) = self.free_segments[i]; - let seg_end = seg_offset + seg_size; - - if seg_end == offset { - // Merge with the preceding segment - self.free_segments[i].1 += size; - // Check if we also need to merge with the following segment - if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { - self.free_segments[i].1 += self.free_segments[i + 1].1; - // TODO: use swap remove - self.free_segments.remove(i + 1); // Remove the merged following segment - } - return; - } else if dealloc_end == seg_offset { - // Merge with the following segment - self.free_segments[i].0 = offset; - self.free_segments[i].1 += size; - return; - } else if seg_offset > offset { - // Insertion point found (segments are sorted by offset) - insert_index = i; - break; - } - } - // If no merge is possible, insert the new segment at the determined index - self.free_segments.insert(insert_index, new_segment); - } } impl FirstFitList { diff --git a/betree/src/allocator/first_fit_scan.rs b/betree/src/allocator/first_fit_scan.rs index 530be1413..77c96bd08 100644 --- a/betree/src/allocator/first_fit_scan.rs +++ b/betree/src/allocator/first_fit_scan.rs @@ -65,12 +65,4 @@ impl Allocator for FirstFitScan { self.mark(offset, size, Action::Allocate); true } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - } } diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index f3e2b471c..7f46a2b81 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -3,6 +3,21 @@ use bitvec::prelude::*; use byteorder::{BigEndian, ByteOrder}; use serde::{Deserialize, Serialize}; +// OPTIM: For all the LIST variants of allocators: try to remove a free segment that can be created +// on allocation. There are two ways: +// 1. call remove on free_segments: this copies all the elements after the removed one, one place +// to the front. This keeps the sorting of the array +// 2. swap remove the segment: swap the empty segment to the back and decrease the len by 1. This +// breaks the sorting but is probably much faster +// The allocate_at function is called quite rarely (only on creation of the database and on loading +// the allocator bitmap from disk) and it's the only reason, why the free_segments are kept sorted +// anyway. So changing that function to respect this change could increase the performance. +// +// OPTIM: For all the SCAN variants of allocators: we could use assembly (and possibly SIMD) to +// count the leading/trailing 0s/1s. This could be faster than relying on the compiler. +// +// OPTIM: For the FSM variants: we could when updating the internal nodes on insertion stop early, +// when we notice, that the offset changes mod first_fit_scan; pub use self::first_fit_scan::FirstFitScan; @@ -124,13 +139,8 @@ pub trait Allocator: Send + Sync { /// This method attempts to allocate a contiguous block of memory at the given offset. fn allocate_at(&mut self, size: u32, offset: u32) -> bool; - /// Deallocates a previously allocated block of memory. - /// - /// This method marks the specified block as free, making it available for - /// future allocations. - fn deallocate(&mut self, offset: u32, size: u32); - /// Marks a range of bits in the bitmap with the given action. + #[inline] fn mark(&mut self, offset: u32, size: u32, action: Action) { let start_idx = offset as usize; let end_idx = (offset + size) as usize; @@ -157,6 +167,7 @@ pub enum Action { impl Action { /// Returns 1 if allocation and 0 if deallocation. + #[inline] pub fn as_bool(self) -> bool { match self { Action::Deallocate => false, @@ -228,9 +239,6 @@ impl SegmentId { #[cfg(test)] mod tests { use super::*; - use rand::{ - distributions::WeightedIndex, prelude::Distribution, rngs::StdRng, Rng, SeedableRng, - }; #[test] fn segment_id() { @@ -245,126 +253,4 @@ mod tests { ); assert_eq!(SegmentId::get_block_offset(offset), 1); } - - fn run_fuzz_tests(allocator: &mut T) { - let mut rng = StdRng::seed_from_u64(0); - - // Store allocation records (offset, size) - let mut allocations: Vec<(u32, u32)> = Vec::new(); - - // Define action weights (allocate: 60%, deallocate: 30%, allocate_at: 10%) - let weights = [60, 30, 10]; - let dist = WeightedIndex::new(&weights).unwrap(); - - for _ in 0..100000 { - let action = dist.sample(&mut rng); - match action { - 0 => { - // Allocate - let size = rng.gen_range(1..256); - if let Some(offset) = allocator.allocate(size) { - allocations.push((offset, size)); - } - } - 1 => { - // Deallocate - if !allocations.is_empty() { - let index = rng.gen_range(0..allocations.len()); - let (offset, size) = allocations.swap_remove(index); // Remove the chosen allocation - allocator.deallocate(offset, size); - } - } - 2 => { - // Allocate at - let offset = rng.gen_range(0..(SEGMENT_SIZE * 2) as u32); - let size = rng.gen_range(1..256); - if allocator.allocate_at(offset, size) { - allocations.push((offset, size)); // Store if successful - } - } - _ => unreachable!(), - } - } - - // TODO: find some generic assertions - } - - macro_rules! generate_fuzz_tests { - ($test_name:ident, $allocator_type:ty) => { - #[test] - fn $test_name() { - let mut allocator = <$allocator_type>::new([0; SEGMENT_SIZE_BYTES]); - run_fuzz_tests(&mut allocator); - } - }; - } - - fn small_tests(allocator: &mut T) { - let offset = allocator.allocate(10); - assert!(offset.is_some()); - - let offset = offset.unwrap(); - allocator.deallocate(offset, 10); - - let success = allocator.allocate_at(5, 10); - assert!(success); - - // Allocation Failure - let offset = allocator.allocate(SEGMENT_SIZE as u32); // Assuming the entire segment isn't free - assert!(offset.is_none()); - - // Out-of-bounds - let success = allocator.allocate_at(SEGMENT_SIZE as u32, 1); - assert!(!success); - - // Zero-sized allocation - let offset = allocator.allocate(0); - assert!(offset.is_some()); - - // Repeated Allocation/Deallocation - for _ in 0..10 { - let offset = allocator.allocate(12).unwrap(); - allocator.deallocate(offset, 12); - } - - // Large Allocation Followed by Small Allocations - let large_offset = allocator.allocate(100).unwrap(); - _ = allocator.allocate(4).unwrap(); - _ = allocator.allocate(7).unwrap(); - allocator.deallocate(large_offset, 100); - } - - macro_rules! generate_small_tests { - ($test_name:ident, $allocator_type:ty) => { - #[test] - fn $test_name() { - let mut allocator = <$allocator_type>::new([0; SEGMENT_SIZE_BYTES]); - small_tests(&mut allocator); - } - }; - } - - // Generate tests for each allocator - generate_small_tests!(test_first_fit_scan, FirstFitScan); - generate_small_tests!(test_first_fit_list, FirstFitList); - generate_small_tests!(test_first_fit_fsm, FirstFitFSM); - generate_small_tests!(test_next_fit_scan, NextFitScan); - generate_small_tests!(test_next_fit_list, NextFitList); - generate_small_tests!(test_best_fit_scan, BestFitScan); - generate_small_tests!(test_best_fit_list, BestFitList); - generate_small_tests!(test_worst_fit_scan, WorstFitScan); - generate_small_tests!(test_worst_fit_list, WorstFitList); - generate_small_tests!(test_segment_allocator, SegmentAllocator); - - // Generate fuzz tests for each allocator - generate_fuzz_tests!(test_first_fit_scan_fuzz, FirstFitScan); - generate_fuzz_tests!(test_first_fit_list_fuzz, FirstFitList); - generate_fuzz_tests!(test_first_fit_fsm_fuzz, FirstFitFSM); - generate_fuzz_tests!(test_next_fit_scan_fuzz, NextFitScan); - generate_fuzz_tests!(test_next_fit_list_fuzz, NextFitList); - generate_fuzz_tests!(test_best_fit_scan_fuzz, BestFitScan); - generate_fuzz_tests!(test_best_fit_list_fuzz, BestFitList); - generate_fuzz_tests!(test_worst_fit_scan_fuzz, WorstFitScan); - generate_fuzz_tests!(test_worst_fit_list_fuzz, WorstFitList); - generate_fuzz_tests!(test_segment_allocator_fuzz, SegmentAllocator); } diff --git a/betree/src/allocator/next_fit_list.rs b/betree/src/allocator/next_fit_list.rs index 7c9fe4b69..8fb5d84bb 100644 --- a/betree/src/allocator/next_fit_list.rs +++ b/betree/src/allocator/next_fit_list.rs @@ -112,46 +112,6 @@ impl Allocator for NextFitList { false } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - - let dealloc_end = offset + size; - let new_segment = (offset, size); - let mut insert_index = self.free_segments.len(); - - // Find the correct insertion index to keep free_segments sorted and check for merging - for i in 0..self.free_segments.len() { - let (seg_offset, seg_size) = self.free_segments[i]; - let seg_end = seg_offset + seg_size; - - if seg_end == offset { - // Merge with the preceding segment - self.free_segments[i].1 += size; - // Check if we can also merge with the following segment - if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { - self.free_segments[i].1 += self.free_segments[i + 1].1; - self.free_segments.remove(i + 1); // Remove the merged following segment - } - return; - } else if dealloc_end == seg_offset { - // Merge with the following segment - self.free_segments[i].0 = offset; - self.free_segments[i].1 += size; - return; - } else if seg_offset > offset { - // Insertion point found (segments are sorted by offset) - insert_index = i; - break; - } - } - // If no merge is possible, insert the new segment at the determined index - self.free_segments.insert(insert_index, new_segment); - } } impl NextFitList { diff --git a/betree/src/allocator/next_fit_scan.rs b/betree/src/allocator/next_fit_scan.rs index 1441443f5..cd644035a 100644 --- a/betree/src/allocator/next_fit_scan.rs +++ b/betree/src/allocator/next_fit_scan.rs @@ -86,12 +86,4 @@ impl Allocator for NextFitScan { self.last_offset = offset + size; true } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - } } diff --git a/betree/src/allocator/segment_allocator.rs b/betree/src/allocator/segment_allocator.rs index 51a628f2e..79d5976b1 100644 --- a/betree/src/allocator/segment_allocator.rs +++ b/betree/src/allocator/segment_allocator.rs @@ -68,12 +68,4 @@ impl Allocator for SegmentAllocator { self.mark(offset, size, Action::Allocate); true } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - } } diff --git a/betree/src/allocator/worst_fit_list.rs b/betree/src/allocator/worst_fit_list.rs index 3ee80eaa4..080991d3f 100644 --- a/betree/src/allocator/worst_fit_list.rs +++ b/betree/src/allocator/worst_fit_list.rs @@ -102,42 +102,6 @@ impl Allocator for WorstFitList { false } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - - let dealloc_end = offset + size; - let new_segment = (offset, size); - let mut insert_index = self.free_segments.len(); - - for i in 0..self.free_segments.len() { - let (seg_offset, seg_size) = self.free_segments[i]; - let seg_end = seg_offset + seg_size; - - if seg_end == offset { - // Merge with the preceding segment - self.free_segments[i].1 += size; - if i + 1 < self.free_segments.len() && self.free_segments[i + 1].0 == dealloc_end { - self.free_segments[i].1 += self.free_segments[i + 1].1; - self.free_segments.remove(i + 1); - } - return; - } else if dealloc_end == seg_offset { - // Merge with the following segment - self.free_segments[i].0 = offset; - self.free_segments[i].1 += size; - return; - } else if seg_offset > offset { - insert_index = i; - break; - } - } - self.free_segments.insert(insert_index, new_segment); - } } impl WorstFitList { diff --git a/betree/src/allocator/worst_fit_scan.rs b/betree/src/allocator/worst_fit_scan.rs index 12d25cee0..e222384b7 100644 --- a/betree/src/allocator/worst_fit_scan.rs +++ b/betree/src/allocator/worst_fit_scan.rs @@ -95,12 +95,4 @@ impl Allocator for WorstFitScan { self.mark(offset, size, Action::Allocate); true } - - /// Deallocates the allocated block. - fn deallocate(&mut self, offset: u32, size: u32) { - if offset + size > SEGMENT_SIZE as u32 { - return; - } - self.mark(offset, size, Action::Deallocate); - } } From a4f265275cb6f98cfbada2f851b6dd272041a5f0 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 18:14:11 +0100 Subject: [PATCH 17/36] best fit fsm allocator --- betree/benches/allocator.rs | 3 +- betree/src/allocator/best_fit_fsm.rs | 309 ++++++++++++++++++++++++++ betree/src/allocator/first_fit_fsm.rs | 2 +- betree/src/allocator/mod.rs | 8 + betree/src/database/handler.rs | 1 + 5 files changed, 321 insertions(+), 2 deletions(-) create mode 100644 betree/src/allocator/best_fit_fsm.rs diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 41d75c240..cd6951e8b 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,5 +1,5 @@ use betree_storage_stack::allocator::{ - self, Allocator, BestFitList, BestFitScan, FirstFitFSM, FirstFitList, FirstFitScan, + self, Allocator, BestFitFSM, BestFitList, BestFitScan, FirstFitFSM, FirstFitList, FirstFitScan, NextFitList, NextFitScan, SegmentAllocator, WorstFitList, WorstFitScan, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; @@ -143,6 +143,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { Box::new(AllocatorBenchmark::::new("next_fit_list")), Box::new(AllocatorBenchmark::::new("best_fit_scan")), Box::new(AllocatorBenchmark::::new("best_fit_list")), + Box::new(AllocatorBenchmark::::new("best_fit_fsm")), Box::new(AllocatorBenchmark::::new("worst_fit_scan")), Box::new(AllocatorBenchmark::::new("worst_fit_list")), Box::new(AllocatorBenchmark::::new("segment")), diff --git a/betree/src/allocator/best_fit_fsm.rs b/betree/src/allocator/best_fit_fsm.rs new file mode 100644 index 000000000..b9edea7ae --- /dev/null +++ b/betree/src/allocator/best_fit_fsm.rs @@ -0,0 +1,309 @@ +use super::*; + +/// Based on the free-space-map allocator from postgresql: +/// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README +/// This is an approximate best fit allocator. It will not always find the best fit but it tries +/// its best. +pub struct BestFitFSM { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space + tree_height: u32, +} + +impl Allocator for BestFitFSM { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `FirstFitFSM` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = BestFitFSM { + data, + fsm_tree: Vec::new(), + tree_height: 0, + }; + allocator.build_fsm_tree(); + allocator + } + + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + if self.fsm_tree.is_empty() || self.fsm_tree[0].1 < size { + return None; // Not enough free space + } + + let mut current_node_index = 0; + while current_node_index < self.fsm_tree.len() / 2 { + // Traverse internal nodes + let left_child_index = 2 * current_node_index + 1; + let right_child_index = 2 * current_node_index + 2; + + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + + match (left_child_value.1 >= size, right_child_value.1 >= size) { + (true, true) => { + // Both children can fit size + if left_child_value.1 < right_child_value.1 { + // Left is better fit + current_node_index = left_child_index; + } else { + // Right is better or equal fit + current_node_index = right_child_index; + } + } + (true, false) => current_node_index = left_child_index, // Only left can fit + (false, true) => current_node_index = right_child_index, // Only right can fit + (false, false) => unreachable!(), // Neither child can fit, stop traversal + } + } + + // current_node_index is now the index of the best-fit leaf node + assert!(current_node_index >= self.fsm_tree.len() / 2); + let (offset, segment_size) = self.fsm_tree[current_node_index]; + + assert!(segment_size >= size); + + self.mark(offset, size, Action::Allocate); + + // Update the segment in the leaf node + self.fsm_tree[current_node_index].0 += size; + self.fsm_tree[current_node_index].1 -= size; + + // Update internal nodes up to the root + let mut current_index = current_node_index; + while current_index > 0 { + current_index = (current_index - 1) / 2; // Index of parent node + let left_child_index = 2 * current_index + 1; + let right_child_index = 2 * current_index + 2; + + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[current_index] = left_child_value + } else { + self.fsm_tree[current_index] = right_child_value + } + } + + return Some(offset); + } + + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + // Because the tree is sorted by offset because of how it's build, this shouldn't be to + // hard to implement efficiently + todo!() + } +} + +impl BestFitFSM { + fn get_free_segments(&mut self) -> Vec<(u32, u32)> { + let mut offset: u32 = 0; + let mut free_segments = Vec::new(); + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + // If bit is 0, it's free + let start_offset = offset; + let mut current_size: u32 = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + free_segments + } + + fn build_fsm_tree(&mut self) { + let leaf_nodes = self.get_free_segments(); + let leaf_nodes_num = leaf_nodes.len(); + + if leaf_nodes_num == 0 { + self.fsm_tree = vec![(0, 0)]; // Root node with 0 free space + return; + } + + // Calculate the size of the FSM tree array. For simplicity we assume complete tree for now. + self.tree_height = (leaf_nodes_num as f64).log2().ceil() as u32; + // Number of nodes in complete binary tree of height h is 2^(h+1) - 1 + let tree_nodes_num = (1 << (self.tree_height + 1)) - 1; + + self.fsm_tree.clear(); + self.fsm_tree.resize(tree_nodes_num as usize, (0, 0)); + + // 1. Initialize leaf nodes in fsm_tree from free_segments + // OPTIM: just use memcpy + for (i, &(offset, size)) in leaf_nodes.iter().enumerate() { + // Leaf nodes are at the end of the fsm_tree array in a complete binary tree + let leaf_index = (tree_nodes_num / 2) + i; + if leaf_index < tree_nodes_num { + // Prevent out-of-bounds access if free_segments.len() is not power of 2 + self.fsm_tree[leaf_index] = (offset, size); + } + } + + // 2. Build internal nodes bottom-up similar to a binary heap + for i in (0..(tree_nodes_num / 2)).rev() { + let left_child_index = 2 * i + 1; + let right_child_index = 2 * i + 2; + + // Default to 0 if index is out of bounds (incomplete tree) + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[i] = left_child_value + } else { + self.fsm_tree[i] = right_child_value + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_empty() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let allocator = BestFitFSM::new(bitmap); + + // In an empty bitmap, the root node should have a large free space + assert_eq!(allocator.fsm_tree[0].0, 0 as u32); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32); + assert_eq!(allocator.tree_height, 0); + } + + #[test] + fn build_simple() { + // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments + bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning + bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones + + let mut allocator = BestFitFSM::new(bitmap.into_inner()); + + let fsm_tree = vec![ + (7, SEGMENT_SIZE as u32 - 7), + (3, 2), + (7, SEGMENT_SIZE as u32 - 7), + ]; + assert_eq!(allocator.fsm_tree, fsm_tree); + assert_eq!(allocator.tree_height, 1); + } + + #[test] + fn build_complex() { + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let allocator = BestFitFSM::new(bitmap.into_inner()); + + // binary heap layout + let fsm_tree = vec![ + (53, SEGMENT_SIZE as u32 - 53), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (10, 4), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (3, 2), + (10, 4), + (22, 13), + (36, 6), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (0, 0), + (0, 0), + ]; + + assert_eq!(fsm_tree, allocator.fsm_tree); + assert_eq!(allocator.tree_height, 3); + } + + #[test] + fn allocate_empty_fsm_tree() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let mut allocator = BestFitFSM::new(bitmap); + + let allocation = allocator.allocate(1024); + assert!(allocation.is_some()); // Allocation should succeed + + let allocated_offset = allocation.unwrap(); + assert_eq!(allocated_offset, 0); // Should allocate at the beginning + + // Check if the allocated region is marked as used in the bitmap + assert!(allocator.data()[0..1024 as usize].all()); + // Check root node value after allocation + assert_eq!(allocator.fsm_tree[0], (1024, SEGMENT_SIZE as u32 - 1024)); + } + + #[test] + fn allocate_complex_fsm_tree() { + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let mut allocator = BestFitFSM::new(bitmap.into_inner()); + + // Best-fit should allocate from the segment at offset 3 with size 2 + let allocation = allocator.allocate(2); // Request allocation of size 2 + assert!(allocation.is_some()); + assert_eq!(allocation.unwrap(), 3); + // Verify that the allocated region is marked in the bitmap + assert!(allocator.data()[3..5].all()); + + let allocation2 = allocator.allocate(10); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 22); + assert!(allocator.data()[22..32].all()); + + // Allocate again, to use the next best fit segment + let allocation2 = allocator.allocate(100); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 53); + assert!(allocator.data()[53..153].all()); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32 - 153); + } + + #[test] + fn allocate_fail_fsm_tree() { + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let root_free_space = allocator.fsm_tree[0].1; + + // Try to allocate more than available space + let allocation = allocator.allocate(root_free_space + 1); + assert!(allocation.is_none()); // Allocation should fail + + // Check if fsm_tree root value is still the same + assert_eq!(allocator.fsm_tree[0].1, root_free_space); // Should remain unchanged + } +} diff --git a/betree/src/allocator/first_fit_fsm.rs b/betree/src/allocator/first_fit_fsm.rs index 87fc583d8..0d36af169 100644 --- a/betree/src/allocator/first_fit_fsm.rs +++ b/betree/src/allocator/first_fit_fsm.rs @@ -64,7 +64,7 @@ impl Allocator for FirstFitFSM { self.mark(offset, size, Action::Allocate); - // Update the free segment size in the leaf node + // Update the segment in the leaf node self.fsm_tree[current_node_index].0 += size; self.fsm_tree[current_node_index].1 -= size; diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 7f46a2b81..de59fcb0a 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -48,6 +48,9 @@ pub use self::worst_fit_list::WorstFitList; mod first_fit_fsm; pub use self::first_fit_fsm::FirstFitFSM; +mod best_fit_fsm; +pub use self::best_fit_fsm::BestFitFSM; + /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap @@ -98,6 +101,11 @@ pub enum AllocatorType { /// segment from this list to allocate memory. BestFitList, + /// **Best Fit FSM:** + /// This allocator builds a binary tree of offsets and sizes, that has the + /// max-heap property on the sizes and uses it to find suitable free space. + BestFitFSM, + /// **Worst Fit Scan:** /// This allocator searches the entire segment and allocates the largest /// free block. This simple version uses a linear search to find the worst diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 430b41f28..51a5bb76b 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -203,6 +203,7 @@ impl Handler { AllocatorType::NextFitList => Box::new(NextFitList::new(bitmap)), AllocatorType::BestFitScan => Box::new(BestFitScan::new(bitmap)), AllocatorType::BestFitList => Box::new(BestFitList::new(bitmap)), + AllocatorType::BestFitFSM => Box::new(BestFitFSM::new(bitmap)), AllocatorType::WorstFitScan => Box::new(WorstFitScan::new(bitmap)), AllocatorType::WorstFitList => Box::new(WorstFitList::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), From e0abd31bf899cf533adf26179f9ecc6f45e233d1 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 18:20:25 +0100 Subject: [PATCH 18/36] worst fit fsm allocator --- betree/benches/allocator.rs | 4 +- betree/src/allocator/best_fit_fsm.rs | 2 +- betree/src/allocator/mod.rs | 8 + betree/src/allocator/worst_fit_fsm.rs | 309 ++++++++++++++++++++++++++ betree/src/database/handler.rs | 1 + 5 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 betree/src/allocator/worst_fit_fsm.rs diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index cd6951e8b..b19ec2fb0 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,6 +1,7 @@ use betree_storage_stack::allocator::{ self, Allocator, BestFitFSM, BestFitList, BestFitScan, FirstFitFSM, FirstFitList, FirstFitScan, - NextFitList, NextFitScan, SegmentAllocator, WorstFitList, WorstFitScan, SEGMENT_SIZE_BYTES, + NextFitList, NextFitScan, SegmentAllocator, WorstFitFSM, WorstFitList, WorstFitScan, + SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::distributions::{Distribution, Uniform}; @@ -146,6 +147,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { Box::new(AllocatorBenchmark::::new("best_fit_fsm")), Box::new(AllocatorBenchmark::::new("worst_fit_scan")), Box::new(AllocatorBenchmark::::new("worst_fit_list")), + Box::new(AllocatorBenchmark::::new("worst_fit_fsm")), Box::new(AllocatorBenchmark::::new("segment")), ]; diff --git a/betree/src/allocator/best_fit_fsm.rs b/betree/src/allocator/best_fit_fsm.rs index b9edea7ae..d01a38c14 100644 --- a/betree/src/allocator/best_fit_fsm.rs +++ b/betree/src/allocator/best_fit_fsm.rs @@ -15,7 +15,7 @@ impl Allocator for BestFitFSM { &mut self.data } - /// Constructs a new `FirstFitFSM` given the segment allocation bitmap. + /// Constructs a new `BestFitFSM` given the segment allocation bitmap. /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index de59fcb0a..52371e68d 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -51,6 +51,9 @@ pub use self::first_fit_fsm::FirstFitFSM; mod best_fit_fsm; pub use self::best_fit_fsm::BestFitFSM; +mod worst_fit_fsm; +pub use self::worst_fit_fsm::WorstFitFSM; + /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap @@ -117,6 +120,11 @@ pub enum AllocatorType { /// (largest) segment from this list to allocate memory. WorstFitList, + /// **Worst Fit FSM:** + /// This allocator builds a binary tree of offsets and sizes, that has the + /// max-heap property on the sizes and uses it to find suitable free space. + WorstFitFSM, + /// **Segment Allocator:** /// This is a first fit allocator that was used before making the allocators /// generic. It is not efficient and mainly included for reference. diff --git a/betree/src/allocator/worst_fit_fsm.rs b/betree/src/allocator/worst_fit_fsm.rs new file mode 100644 index 000000000..bf090a5a0 --- /dev/null +++ b/betree/src/allocator/worst_fit_fsm.rs @@ -0,0 +1,309 @@ +use super::*; + +/// Based on the free-space-map allocator from postgresql: +/// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README +/// This is an approximate worst fit allocator. It will not always find the worst fit but it tries +/// its best. +pub struct WorstFitFSM { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space + tree_height: u32, +} + +impl Allocator for WorstFitFSM { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `WorstFitFSM` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = WorstFitFSM { + data, + fsm_tree: Vec::new(), + tree_height: 0, + }; + allocator.build_fsm_tree(); + allocator + } + + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + if self.fsm_tree.is_empty() || self.fsm_tree[0].1 < size { + return None; // Not enough free space + } + + let mut current_node_index = 0; + while current_node_index < self.fsm_tree.len() / 2 { + // Traverse internal nodes + let left_child_index = 2 * current_node_index + 1; + let right_child_index = 2 * current_node_index + 2; + + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + + match (left_child_value.1 >= size, right_child_value.1 >= size) { + (true, true) => { + // Both children can fit size + if left_child_value.1 < right_child_value.1 { + // Right is worse fit + current_node_index = right_child_index; + } else { + // Left is worse or equal fit + current_node_index = left_child_index; + } + } + (true, false) => current_node_index = left_child_index, // Only left can fit + (false, true) => current_node_index = right_child_index, // Only right can fit + (false, false) => unreachable!(), // Neither child can fit, stop traversal + } + } + + // current_node_index is now the index of the worst-fit leaf node + assert!(current_node_index >= self.fsm_tree.len() / 2); + let (offset, segment_size) = self.fsm_tree[current_node_index]; + + assert!(segment_size >= size); + + self.mark(offset, size, Action::Allocate); + + // Update the segment in the leaf node + self.fsm_tree[current_node_index].0 += size; + self.fsm_tree[current_node_index].1 -= size; + + // Update internal nodes up to the root + let mut current_index = current_node_index; + while current_index > 0 { + current_index = (current_index - 1) / 2; // Index of parent node + let left_child_index = 2 * current_index + 1; + let right_child_index = 2 * current_index + 2; + + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[current_index] = left_child_value + } else { + self.fsm_tree[current_index] = right_child_value + } + } + + return Some(offset); + } + + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + // Because the tree is sorted by offset because of how it's build, this shouldn't be to + // hard to implement efficiently + todo!() + } +} + +impl WorstFitFSM { + fn get_free_segments(&mut self) -> Vec<(u32, u32)> { + let mut offset: u32 = 0; + let mut free_segments = Vec::new(); + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + // If bit is 0, it's free + let start_offset = offset; + let mut current_size: u32 = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + free_segments + } + + fn build_fsm_tree(&mut self) { + let leaf_nodes = self.get_free_segments(); + let leaf_nodes_num = leaf_nodes.len(); + + if leaf_nodes_num == 0 { + self.fsm_tree = vec![(0, 0)]; // Root node with 0 free space + return; + } + + // Calculate the size of the FSM tree array. For simplicity we assume complete tree for now. + self.tree_height = (leaf_nodes_num as f64).log2().ceil() as u32; + // Number of nodes in complete binary tree of height h is 2^(h+1) - 1 + let tree_nodes_num = (1 << (self.tree_height + 1)) - 1; + + self.fsm_tree.clear(); + self.fsm_tree.resize(tree_nodes_num as usize, (0, 0)); + + // 1. Initialize leaf nodes in fsm_tree from free_segments + // OPTIM: just use memcpy + for (i, &(offset, size)) in leaf_nodes.iter().enumerate() { + // Leaf nodes are at the end of the fsm_tree array in a complete binary tree + let leaf_index = (tree_nodes_num / 2) + i; + if leaf_index < tree_nodes_num { + // Prevent out-of-bounds access if free_segments.len() is not power of 2 + self.fsm_tree[leaf_index] = (offset, size); + } + } + + // 2. Build internal nodes bottom-up similar to a binary heap + for i in (0..(tree_nodes_num / 2)).rev() { + let left_child_index = 2 * i + 1; + let right_child_index = 2 * i + 2; + + // Default to 0 if index is out of bounds (incomplete tree) + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[i] = left_child_value + } else { + self.fsm_tree[i] = right_child_value + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_empty() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let allocator = WorstFitFSM::new(bitmap); + + // In an empty bitmap, the root node should have a large free space + assert_eq!(allocator.fsm_tree[0].0, 0 as u32); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32); + assert_eq!(allocator.tree_height, 0); + } + + #[test] + fn build_simple() { + // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments + bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning + bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones + + let mut allocator = WorstFitFSM::new(bitmap.into_inner()); + + let fsm_tree = vec![ + (7, SEGMENT_SIZE as u32 - 7), + (3, 2), + (7, SEGMENT_SIZE as u32 - 7), + ]; + assert_eq!(allocator.fsm_tree, fsm_tree); + assert_eq!(allocator.tree_height, 1); + } + + #[test] + fn build_complex() { + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let allocator = WorstFitFSM::new(bitmap.into_inner()); + + // binary heap layout + let fsm_tree = vec![ + (53, SEGMENT_SIZE as u32 - 53), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (10, 4), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (3, 2), + (10, 4), + (22, 13), + (36, 6), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (0, 0), + (0, 0), + ]; + + assert_eq!(fsm_tree, allocator.fsm_tree); + assert_eq!(allocator.tree_height, 3); + } + + #[test] + fn allocate_empty_fsm_tree() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let mut allocator = WorstFitFSM::new(bitmap); + + let allocation = allocator.allocate(1024); + assert!(allocation.is_some()); // Allocation should succeed + + let allocated_offset = allocation.unwrap(); + assert_eq!(allocated_offset, 0); // Should allocate at the beginning + + // Check if the allocated region is marked as used in the bitmap + assert!(allocator.data()[0..1024 as usize].all()); + // Check root node value after allocation + assert_eq!(allocator.fsm_tree[0], (1024, SEGMENT_SIZE as u32 - 1024)); + } + + #[test] + fn allocate_complex_fsm_tree() { + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let mut allocator = WorstFitFSM::new(bitmap.into_inner()); + + // Worst-fit should allocate from the segment at offset 3 with size 2 + let allocation = allocator.allocate(2); // Request allocation of size 2 + assert!(allocation.is_some()); + assert_eq!(allocation.unwrap(), 53); + // Verify that the allocated region is marked in the bitmap + assert!(allocator.data()[53..55].all()); + + let allocation2 = allocator.allocate(10); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 55); + assert!(allocator.data()[55..65].all()); + + // Allocate again, to use the next worst fit segment + let allocation2 = allocator.allocate(100); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 65); + assert!(allocator.data()[65..165].all()); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32 - 165); + } + + #[test] + fn allocate_fail_fsm_tree() { + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let root_free_space = allocator.fsm_tree[0].1; + + // Try to allocate more than available space + let allocation = allocator.allocate(root_free_space + 1); + assert!(allocation.is_none()); // Allocation should fail + + // Check if fsm_tree root value is still the same + assert_eq!(allocator.fsm_tree[0].1, root_free_space); // Should remain unchanged + } +} diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 51a5bb76b..e65d806d0 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -206,6 +206,7 @@ impl Handler { AllocatorType::BestFitFSM => Box::new(BestFitFSM::new(bitmap)), AllocatorType::WorstFitScan => Box::new(WorstFitScan::new(bitmap)), AllocatorType::WorstFitList => Box::new(WorstFitList::new(bitmap)), + AllocatorType::WorstFitFSM => Box::new(WorstFitFSM::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From aac3a748dbc1c604b2cb8de3db843bdd1a50bb6a Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 24 Jan 2025 22:06:43 +0100 Subject: [PATCH 19/36] rename fsm to tree --- betree/benches/allocator.rs | 12 ++++----- .../{best_fit_fsm.rs => best_fit_tree.rs} | 26 +++++++++---------- .../{first_fit_fsm.rs => first_fit_tree.rs} | 26 +++++++++---------- betree/src/allocator/mod.rs | 13 +++++----- .../{worst_fit_fsm.rs => worst_fit_tree.rs} | 26 +++++++++---------- betree/src/database/handler.rs | 6 ++--- 6 files changed, 55 insertions(+), 54 deletions(-) rename betree/src/allocator/{best_fit_fsm.rs => best_fit_tree.rs} (93%) rename betree/src/allocator/{first_fit_fsm.rs => first_fit_tree.rs} (93%) rename betree/src/allocator/{worst_fit_fsm.rs => worst_fit_tree.rs} (93%) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index b19ec2fb0..bd5ea445f 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,7 +1,7 @@ use betree_storage_stack::allocator::{ - self, Allocator, BestFitFSM, BestFitList, BestFitScan, FirstFitFSM, FirstFitList, FirstFitScan, - NextFitList, NextFitScan, SegmentAllocator, WorstFitFSM, WorstFitList, WorstFitScan, - SEGMENT_SIZE_BYTES, + self, Allocator, BestFitList, BestFitScan, BestFitTree, FirstFitList, FirstFitScan, + FirstFitTree, NextFitList, NextFitScan, SegmentAllocator, WorstFitList, WorstFitScan, + WorstFitTree, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::distributions::{Distribution, Uniform}; @@ -139,15 +139,15 @@ pub fn criterion_benchmark(c: &mut Criterion) { let allocator_benchmarks: Vec> = vec![ Box::new(AllocatorBenchmark::::new("first_fit_scan")), Box::new(AllocatorBenchmark::::new("first_fit_list")), - Box::new(AllocatorBenchmark::::new("first_fit_fsm")), + Box::new(AllocatorBenchmark::::new("first_fit_fsm")), Box::new(AllocatorBenchmark::::new("next_fit_scan")), Box::new(AllocatorBenchmark::::new("next_fit_list")), Box::new(AllocatorBenchmark::::new("best_fit_scan")), Box::new(AllocatorBenchmark::::new("best_fit_list")), - Box::new(AllocatorBenchmark::::new("best_fit_fsm")), + Box::new(AllocatorBenchmark::::new("best_fit_fsm")), Box::new(AllocatorBenchmark::::new("worst_fit_scan")), Box::new(AllocatorBenchmark::::new("worst_fit_list")), - Box::new(AllocatorBenchmark::::new("worst_fit_fsm")), + Box::new(AllocatorBenchmark::::new("worst_fit_fsm")), Box::new(AllocatorBenchmark::::new("segment")), ]; diff --git a/betree/src/allocator/best_fit_fsm.rs b/betree/src/allocator/best_fit_tree.rs similarity index 93% rename from betree/src/allocator/best_fit_fsm.rs rename to betree/src/allocator/best_fit_tree.rs index d01a38c14..21df4e221 100644 --- a/betree/src/allocator/best_fit_fsm.rs +++ b/betree/src/allocator/best_fit_tree.rs @@ -4,13 +4,13 @@ use super::*; /// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README /// This is an approximate best fit allocator. It will not always find the best fit but it tries /// its best. -pub struct BestFitFSM { +pub struct BestFitTree { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space tree_height: u32, } -impl Allocator for BestFitFSM { +impl Allocator for BestFitTree { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -19,7 +19,7 @@ impl Allocator for BestFitFSM { /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); - let mut allocator = BestFitFSM { + let mut allocator = BestFitTree { data, fsm_tree: Vec::new(), tree_height: 0, @@ -101,7 +101,7 @@ impl Allocator for BestFitFSM { } } -impl BestFitFSM { +impl BestFitTree { fn get_free_segments(&mut self) -> Vec<(u32, u32)> { let mut offset: u32 = 0; let mut free_segments = Vec::new(); @@ -175,7 +175,7 @@ mod tests { #[test] fn build_empty() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let allocator = BestFitFSM::new(bitmap); + let allocator = BestFitTree::new(bitmap); // In an empty bitmap, the root node should have a large free space assert_eq!(allocator.fsm_tree[0].0, 0 as u32); @@ -186,14 +186,14 @@ mod tests { #[test] fn build_simple() { // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free - let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones - let mut allocator = BestFitFSM::new(bitmap.into_inner()); + let mut allocator = BestFitTree::new(bitmap.into_inner()); let fsm_tree = vec![ (7, SEGMENT_SIZE as u32 - 7), @@ -206,7 +206,7 @@ mod tests { #[test] fn build_complex() { - let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -217,7 +217,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let allocator = BestFitFSM::new(bitmap.into_inner()); + let allocator = BestFitTree::new(bitmap.into_inner()); // binary heap layout let fsm_tree = vec![ @@ -245,7 +245,7 @@ mod tests { #[test] fn allocate_empty_fsm_tree() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let mut allocator = BestFitFSM::new(bitmap); + let mut allocator = BestFitTree::new(bitmap); let allocation = allocator.allocate(1024); assert!(allocation.is_some()); // Allocation should succeed @@ -261,7 +261,7 @@ mod tests { #[test] fn allocate_complex_fsm_tree() { - let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -272,7 +272,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let mut allocator = BestFitFSM::new(bitmap.into_inner()); + let mut allocator = BestFitTree::new(bitmap.into_inner()); // Best-fit should allocate from the segment at offset 3 with size 2 let allocation = allocator.allocate(2); // Request allocation of size 2 @@ -296,7 +296,7 @@ mod tests { #[test] fn allocate_fail_fsm_tree() { - let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let root_free_space = allocator.fsm_tree[0].1; // Try to allocate more than available space diff --git a/betree/src/allocator/first_fit_fsm.rs b/betree/src/allocator/first_fit_tree.rs similarity index 93% rename from betree/src/allocator/first_fit_fsm.rs rename to betree/src/allocator/first_fit_tree.rs index 0d36af169..751070279 100644 --- a/betree/src/allocator/first_fit_fsm.rs +++ b/betree/src/allocator/first_fit_tree.rs @@ -2,13 +2,13 @@ use super::*; /// Based on the free-space-map allocator from postgresql: /// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README -pub struct FirstFitFSM { +pub struct FirstFitTree { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space tree_height: u32, } -impl Allocator for FirstFitFSM { +impl Allocator for FirstFitTree { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -17,7 +17,7 @@ impl Allocator for FirstFitFSM { /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); - let mut allocator = FirstFitFSM { + let mut allocator = FirstFitTree { data, fsm_tree: Vec::new(), tree_height: 0, @@ -94,7 +94,7 @@ impl Allocator for FirstFitFSM { } } -impl FirstFitFSM { +impl FirstFitTree { fn get_free_segments(&mut self) -> Vec<(u32, u32)> { let mut offset: u32 = 0; let mut free_segments = Vec::new(); @@ -168,7 +168,7 @@ mod tests { #[test] fn build_empty() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let allocator = FirstFitFSM::new(bitmap); + let allocator = FirstFitTree::new(bitmap); // In an empty bitmap, the root node should have a large free space assert_eq!(allocator.fsm_tree[0].0, 0 as u32); @@ -179,14 +179,14 @@ mod tests { #[test] fn build_simple() { // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free - let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones - let mut allocator = FirstFitFSM::new(bitmap.into_inner()); + let mut allocator = FirstFitTree::new(bitmap.into_inner()); let fsm_tree = vec![ (7, SEGMENT_SIZE as u32 - 7), @@ -199,7 +199,7 @@ mod tests { #[test] fn build_complex() { - let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -210,7 +210,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let allocator = FirstFitFSM::new(bitmap.into_inner()); + let allocator = FirstFitTree::new(bitmap.into_inner()); // binary heap layout let fsm_tree = vec![ @@ -238,7 +238,7 @@ mod tests { #[test] fn allocate_empty_fsm_tree() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let mut allocator = FirstFitFSM::new(bitmap); + let mut allocator = FirstFitTree::new(bitmap); let allocation = allocator.allocate(1024); assert!(allocation.is_some()); // Allocation should succeed @@ -254,7 +254,7 @@ mod tests { #[test] fn allocate_complex_fsm_tree() { - let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -265,7 +265,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let mut allocator = FirstFitFSM::new(bitmap.into_inner()); + let mut allocator = FirstFitTree::new(bitmap.into_inner()); // Best-fit should allocate from the segment at offset 3 with size 2 let allocation = allocator.allocate(2); // Request allocation of size 2 @@ -289,7 +289,7 @@ mod tests { #[test] fn allocate_fail_fsm_tree() { - let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let root_free_space = allocator.fsm_tree[0].1; // Try to allocate more than available space diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 52371e68d..e8a20ab24 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -45,14 +45,14 @@ pub use self::best_fit_list::BestFitList; mod worst_fit_list; pub use self::worst_fit_list::WorstFitList; -mod first_fit_fsm; -pub use self::first_fit_fsm::FirstFitFSM; +mod first_fit_tree; +pub use self::first_fit_tree::FirstFitTree; -mod best_fit_fsm; -pub use self::best_fit_fsm::BestFitFSM; +mod best_fit_tree; +pub use self::best_fit_tree::BestFitTree; -mod worst_fit_fsm; -pub use self::worst_fit_fsm::WorstFitFSM; +mod worst_fit_tree; +pub use self::worst_fit_tree::WorstFitTree; /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; @@ -153,6 +153,7 @@ pub trait Allocator: Send + Sync { /// Allocates a block of memory of the given `size` at the specified `offset`. /// /// This method attempts to allocate a contiguous block of memory at the given offset. + /// TODO: investigate if we need this method at all fn allocate_at(&mut self, size: u32, offset: u32) -> bool; /// Marks a range of bits in the bitmap with the given action. diff --git a/betree/src/allocator/worst_fit_fsm.rs b/betree/src/allocator/worst_fit_tree.rs similarity index 93% rename from betree/src/allocator/worst_fit_fsm.rs rename to betree/src/allocator/worst_fit_tree.rs index bf090a5a0..66b5b5174 100644 --- a/betree/src/allocator/worst_fit_fsm.rs +++ b/betree/src/allocator/worst_fit_tree.rs @@ -4,13 +4,13 @@ use super::*; /// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README /// This is an approximate worst fit allocator. It will not always find the worst fit but it tries /// its best. -pub struct WorstFitFSM { +pub struct WorstFitTree { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space tree_height: u32, } -impl Allocator for WorstFitFSM { +impl Allocator for WorstFitTree { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -19,7 +19,7 @@ impl Allocator for WorstFitFSM { /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); - let mut allocator = WorstFitFSM { + let mut allocator = WorstFitTree { data, fsm_tree: Vec::new(), tree_height: 0, @@ -101,7 +101,7 @@ impl Allocator for WorstFitFSM { } } -impl WorstFitFSM { +impl WorstFitTree { fn get_free_segments(&mut self) -> Vec<(u32, u32)> { let mut offset: u32 = 0; let mut free_segments = Vec::new(); @@ -175,7 +175,7 @@ mod tests { #[test] fn build_empty() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let allocator = WorstFitFSM::new(bitmap); + let allocator = WorstFitTree::new(bitmap); // In an empty bitmap, the root node should have a large free space assert_eq!(allocator.fsm_tree[0].0, 0 as u32); @@ -186,14 +186,14 @@ mod tests { #[test] fn build_simple() { // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free - let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones - let mut allocator = WorstFitFSM::new(bitmap.into_inner()); + let mut allocator = WorstFitTree::new(bitmap.into_inner()); let fsm_tree = vec![ (7, SEGMENT_SIZE as u32 - 7), @@ -206,7 +206,7 @@ mod tests { #[test] fn build_complex() { - let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -217,7 +217,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let allocator = WorstFitFSM::new(bitmap.into_inner()); + let allocator = WorstFitTree::new(bitmap.into_inner()); // binary heap layout let fsm_tree = vec![ @@ -245,7 +245,7 @@ mod tests { #[test] fn allocate_empty_fsm_tree() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let mut allocator = WorstFitFSM::new(bitmap); + let mut allocator = WorstFitTree::new(bitmap); let allocation = allocator.allocate(1024); assert!(allocation.is_some()); // Allocation should succeed @@ -261,7 +261,7 @@ mod tests { #[test] fn allocate_complex_fsm_tree() { - let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -272,7 +272,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let mut allocator = WorstFitFSM::new(bitmap.into_inner()); + let mut allocator = WorstFitTree::new(bitmap.into_inner()); // Worst-fit should allocate from the segment at offset 3 with size 2 let allocation = allocator.allocate(2); // Request allocation of size 2 @@ -296,7 +296,7 @@ mod tests { #[test] fn allocate_fail_fsm_tree() { - let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); let root_free_space = allocator.fsm_tree[0].1; // Try to allocate more than available space diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index e65d806d0..62e77e86d 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -198,15 +198,15 @@ impl Handler { let mut allocator: Box = match self.allocator { AllocatorType::FirstFitScan => Box::new(FirstFitScan::new(bitmap)), AllocatorType::FirstFitList => Box::new(FirstFitList::new(bitmap)), - AllocatorType::FirstFitFSM => Box::new(FirstFitFSM::new(bitmap)), + AllocatorType::FirstFitFSM => Box::new(FirstFitTree::new(bitmap)), AllocatorType::NextFitScan => Box::new(NextFitScan::new(bitmap)), AllocatorType::NextFitList => Box::new(NextFitList::new(bitmap)), AllocatorType::BestFitScan => Box::new(BestFitScan::new(bitmap)), AllocatorType::BestFitList => Box::new(BestFitList::new(bitmap)), - AllocatorType::BestFitFSM => Box::new(BestFitFSM::new(bitmap)), + AllocatorType::BestFitFSM => Box::new(BestFitTree::new(bitmap)), AllocatorType::WorstFitScan => Box::new(WorstFitScan::new(bitmap)), AllocatorType::WorstFitList => Box::new(WorstFitList::new(bitmap)), - AllocatorType::WorstFitFSM => Box::new(WorstFitFSM::new(bitmap)), + AllocatorType::WorstFitFSM => Box::new(WorstFitTree::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From 129da33800e8450c0298691a6b4d9edc9a3e50d0 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Sun, 26 Jan 2025 11:40:51 +0100 Subject: [PATCH 20/36] real best fit tree allocator --- betree/benches/allocator.rs | 15 +- .../allocator/approximate_best_fit_tree.rs | 309 ++++++++++++++++++ betree/src/allocator/best_fit_tree.rs | 299 +++-------------- betree/src/allocator/mod.rs | 24 +- betree/src/allocator/worst_fit_tree.rs | 2 - betree/src/database/handler.rs | 7 +- 6 files changed, 388 insertions(+), 268 deletions(-) create mode 100644 betree/src/allocator/approximate_best_fit_tree.rs diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index bd5ea445f..2e56ece88 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,7 +1,7 @@ use betree_storage_stack::allocator::{ - self, Allocator, BestFitList, BestFitScan, BestFitTree, FirstFitList, FirstFitScan, - FirstFitTree, NextFitList, NextFitScan, SegmentAllocator, WorstFitList, WorstFitScan, - WorstFitTree, SEGMENT_SIZE_BYTES, + self, Allocator, ApproximateBestFitTree, BestFitList, BestFitScan, BestFitTree, FirstFitList, + FirstFitScan, FirstFitTree, NextFitList, NextFitScan, SegmentAllocator, WorstFitList, + WorstFitScan, WorstFitTree, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::distributions::{Distribution, Uniform}; @@ -139,15 +139,18 @@ pub fn criterion_benchmark(c: &mut Criterion) { let allocator_benchmarks: Vec> = vec![ Box::new(AllocatorBenchmark::::new("first_fit_scan")), Box::new(AllocatorBenchmark::::new("first_fit_list")), - Box::new(AllocatorBenchmark::::new("first_fit_fsm")), + Box::new(AllocatorBenchmark::::new("first_fit_tree")), Box::new(AllocatorBenchmark::::new("next_fit_scan")), Box::new(AllocatorBenchmark::::new("next_fit_list")), Box::new(AllocatorBenchmark::::new("best_fit_scan")), Box::new(AllocatorBenchmark::::new("best_fit_list")), - Box::new(AllocatorBenchmark::::new("best_fit_fsm")), + Box::new(AllocatorBenchmark::::new( + "approximate_best_fit_tree", + )), + Box::new(AllocatorBenchmark::::new("best_fit_tree")), Box::new(AllocatorBenchmark::::new("worst_fit_scan")), Box::new(AllocatorBenchmark::::new("worst_fit_list")), - Box::new(AllocatorBenchmark::::new("worst_fit_fsm")), + Box::new(AllocatorBenchmark::::new("worst_fit_tree")), Box::new(AllocatorBenchmark::::new("segment")), ]; diff --git a/betree/src/allocator/approximate_best_fit_tree.rs b/betree/src/allocator/approximate_best_fit_tree.rs new file mode 100644 index 000000000..055c7262c --- /dev/null +++ b/betree/src/allocator/approximate_best_fit_tree.rs @@ -0,0 +1,309 @@ +use super::*; + +/// Based on the free-space-map allocator from postgresql: +/// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README +/// This is an approximate best fit allocator. It will not always find the best fit but it tries +/// its best. +pub struct ApproximateBestFitTree { + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space + tree_height: u32, +} + +impl Allocator for ApproximateBestFitTree { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + /// Constructs a new `BestFitFSM` given the segment allocation bitmap. + /// The `bitmap` must have a length of `SEGMENT_SIZE`. + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { + let data = BitArray::new(bitmap); + let mut allocator = ApproximateBestFitTree { + data, + fsm_tree: Vec::new(), + tree_height: 0, + }; + allocator.build_fsm_tree(); + allocator + } + + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + if self.fsm_tree.is_empty() || self.fsm_tree[0].1 < size { + return None; // Not enough free space + } + + let mut current_node_index = 0; + while current_node_index < self.fsm_tree.len() / 2 { + // Traverse internal nodes + let left_child_index = 2 * current_node_index + 1; + let right_child_index = 2 * current_node_index + 2; + + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + + match (left_child_value.1 >= size, right_child_value.1 >= size) { + (true, true) => { + // Both children can fit size + if left_child_value.1 < right_child_value.1 { + // Left is better fit + current_node_index = left_child_index; + } else { + // Right is better or equal fit + current_node_index = right_child_index; + } + } + (true, false) => current_node_index = left_child_index, // Only left can fit + (false, true) => current_node_index = right_child_index, // Only right can fit + (false, false) => unreachable!(), // Neither child can fit, stop traversal + } + } + + // current_node_index is now the index of the best-fit leaf node + assert!(current_node_index >= self.fsm_tree.len() / 2); + let (offset, segment_size) = self.fsm_tree[current_node_index]; + + assert!(segment_size >= size); + + self.mark(offset, size, Action::Allocate); + + // Update the segment in the leaf node + self.fsm_tree[current_node_index].0 += size; + self.fsm_tree[current_node_index].1 -= size; + + // Update internal nodes up to the root + let mut current_index = current_node_index; + while current_index > 0 { + current_index = (current_index - 1) / 2; // Index of parent node + let left_child_index = 2 * current_index + 1; + let right_child_index = 2 * current_index + 2; + + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[current_index] = left_child_value + } else { + self.fsm_tree[current_index] = right_child_value + } + } + + return Some(offset); + } + + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + // Because the tree is sorted by offset because of how it's build, this shouldn't be to + // hard to implement efficiently + todo!() + } +} + +impl ApproximateBestFitTree { + fn get_free_segments(&mut self) -> Vec<(u32, u32)> { + let mut offset: u32 = 0; + let mut free_segments = Vec::new(); + while offset < SEGMENT_SIZE as u32 { + if !self.data()[offset as usize] { + // If bit is 0, it's free + let start_offset = offset; + let mut current_size: u32 = 0; + while offset < SEGMENT_SIZE as u32 && !self.data()[offset as usize] { + current_size += 1; + offset += 1; + } + free_segments.push((start_offset, current_size)); + } else { + offset += 1; + } + } + free_segments + } + + fn build_fsm_tree(&mut self) { + let leaf_nodes = self.get_free_segments(); + let leaf_nodes_num = leaf_nodes.len(); + + if leaf_nodes_num == 0 { + self.fsm_tree = vec![(0, 0)]; // Root node with 0 free space + return; + } + + // Calculate the size of the FSM tree array. For simplicity we assume complete tree for now. + self.tree_height = (leaf_nodes_num as f64).log2().ceil() as u32; + // Number of nodes in complete binary tree of height h is 2^(h+1) - 1 + let tree_nodes_num = (1 << (self.tree_height + 1)) - 1; + + self.fsm_tree.clear(); + self.fsm_tree.resize(tree_nodes_num as usize, (0, 0)); + + // 1. Initialize leaf nodes in fsm_tree from free_segments + // OPTIM: just use memcpy + for (i, &(offset, size)) in leaf_nodes.iter().enumerate() { + // Leaf nodes are at the end of the fsm_tree array in a complete binary tree + let leaf_index = (tree_nodes_num / 2) + i; + if leaf_index < tree_nodes_num { + // Prevent out-of-bounds access if free_segments.len() is not power of 2 + self.fsm_tree[leaf_index] = (offset, size); + } + } + + // 2. Build internal nodes bottom-up similar to a binary heap + for i in (0..(tree_nodes_num / 2)).rev() { + let left_child_index = 2 * i + 1; + let right_child_index = 2 * i + 2; + + // Default to 0 if index is out of bounds (incomplete tree) + let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); + let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + + if left_child_value.1 > right_child_value.1 { + self.fsm_tree[i] = left_child_value + } else { + self.fsm_tree[i] = right_child_value + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn build_empty() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let allocator = ApproximateBestFitTree::new(bitmap); + + // In an empty bitmap, the root node should have a large free space + assert_eq!(allocator.fsm_tree[0].0, 0 as u32); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32); + assert_eq!(allocator.tree_height, 0); + } + + #[test] + fn build_simple() { + // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free + let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments + bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning + bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones + + let mut allocator = ApproximateBestFitTree::new(bitmap.into_inner()); + + let fsm_tree = vec![ + (7, SEGMENT_SIZE as u32 - 7), + (3, 2), + (7, SEGMENT_SIZE as u32 - 7), + ]; + assert_eq!(allocator.fsm_tree, fsm_tree); + assert_eq!(allocator.tree_height, 1); + } + + #[test] + fn build_complex() { + let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let allocator = ApproximateBestFitTree::new(bitmap.into_inner()); + + // binary heap layout + let fsm_tree = vec![ + (53, SEGMENT_SIZE as u32 - 53), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (10, 4), + (22, 13), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (3, 2), + (10, 4), + (22, 13), + (36, 6), + (53, SEGMENT_SIZE as u32 - 53), + (0, 0), + (0, 0), + (0, 0), + ]; + + assert_eq!(fsm_tree, allocator.fsm_tree); + assert_eq!(allocator.tree_height, 3); + } + + #[test] + fn allocate_empty_fsm_tree() { + let bitmap = [0u8; SEGMENT_SIZE_BYTES]; + let mut allocator = ApproximateBestFitTree::new(bitmap); + + let allocation = allocator.allocate(1024); + assert!(allocation.is_some()); // Allocation should succeed + + let allocated_offset = allocation.unwrap(); + assert_eq!(allocated_offset, 0); // Should allocate at the beginning + + // Check if the allocated region is marked as used in the bitmap + assert!(allocator.data()[0..1024 as usize].all()); + // Check root node value after allocation + assert_eq!(allocator.fsm_tree[0], (1024, SEGMENT_SIZE as u32 - 1024)); + } + + #[test] + fn allocate_complex_fsm_tree() { + let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let bitmap = allocator.data(); + + // Manually allocate some segments to create a non-trivial tree + bitmap[0..3].fill(true); + bitmap[5..8].fill(true); + bitmap[8..10].fill(true); + bitmap[14..22].fill(true); + bitmap[35..36].fill(true); + bitmap[42..53].fill(true); + + let mut allocator = ApproximateBestFitTree::new(bitmap.into_inner()); + + // Best-fit should allocate from the segment at offset 3 with size 2 + let allocation = allocator.allocate(2); // Request allocation of size 2 + assert!(allocation.is_some()); + assert_eq!(allocation.unwrap(), 3); + // Verify that the allocated region is marked in the bitmap + assert!(allocator.data()[3..5].all()); + + let allocation2 = allocator.allocate(10); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 22); + assert!(allocator.data()[22..32].all()); + + // Allocate again, to use the next best fit segment + let allocation2 = allocator.allocate(100); + assert!(allocation2.is_some()); + assert_eq!(allocation2.unwrap(), 53); + assert!(allocator.data()[53..153].all()); + assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32 - 153); + } + + #[test] + fn allocate_fail_fsm_tree() { + let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let root_free_space = allocator.fsm_tree[0].1; + + // Try to allocate more than available space + let allocation = allocator.allocate(root_free_space + 1); + assert!(allocation.is_none()); // Allocation should fail + + // Check if fsm_tree root value is still the same + assert_eq!(allocator.fsm_tree[0].1, root_free_space); // Should remain unchanged + } +} diff --git a/betree/src/allocator/best_fit_tree.rs b/betree/src/allocator/best_fit_tree.rs index 21df4e221..6746c261d 100644 --- a/betree/src/allocator/best_fit_tree.rs +++ b/betree/src/allocator/best_fit_tree.rs @@ -1,13 +1,11 @@ +use std::collections::BTreeMap; + use super::*; -/// Based on the free-space-map allocator from postgresql: -/// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README -/// This is an approximate best fit allocator. It will not always find the best fit but it tries -/// its best. +/// This is a true best fit allocator. It will always find the best fit if available. pub struct BestFitTree { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), - fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space - tree_height: u32, + tree: BTreeMap>, // store free segments sorted by size (size, offset) } impl Allocator for BestFitTree { @@ -15,16 +13,15 @@ impl Allocator for BestFitTree { &mut self.data } - /// Constructs a new `BestFitFSM` given the segment allocation bitmap. + /// Constructs a new `BestFitTree` given the segment allocation bitmap. /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); let mut allocator = BestFitTree { data, - fsm_tree: Vec::new(), - tree_height: 0, + tree: BTreeMap::new(), }; - allocator.build_fsm_tree(); + allocator.build_tree(); allocator } @@ -33,78 +30,49 @@ impl Allocator for BestFitTree { return Some(0); } - if self.fsm_tree.is_empty() || self.fsm_tree[0].1 < size { - return None; // Not enough free space - } - - let mut current_node_index = 0; - while current_node_index < self.fsm_tree.len() / 2 { - // Traverse internal nodes - let left_child_index = 2 * current_node_index + 1; - let right_child_index = 2 * current_node_index + 2; + if let Some((&segment_size, offsets)) = self.tree.range(size..).next() { + let best_fit_offset = *offsets.first().unwrap(); + self.mark(best_fit_offset, size, Action::Allocate); - let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); - let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); - - match (left_child_value.1 >= size, right_child_value.1 >= size) { - (true, true) => { - // Both children can fit size - if left_child_value.1 < right_child_value.1 { - // Left is better fit - current_node_index = left_child_index; - } else { - // Right is better or equal fit - current_node_index = right_child_index; - } - } - (true, false) => current_node_index = left_child_index, // Only left can fit - (false, true) => current_node_index = right_child_index, // Only right can fit - (false, false) => unreachable!(), // Neither child can fit, stop traversal + // Update free segments tree + self.remove_free_segment(segment_size, best_fit_offset); + if segment_size > size { + self.insert_free_segment(segment_size - size, best_fit_offset + size); } - } - - // current_node_index is now the index of the best-fit leaf node - assert!(current_node_index >= self.fsm_tree.len() / 2); - let (offset, segment_size) = self.fsm_tree[current_node_index]; - assert!(segment_size >= size); - - self.mark(offset, size, Action::Allocate); - - // Update the segment in the leaf node - self.fsm_tree[current_node_index].0 += size; - self.fsm_tree[current_node_index].1 -= size; - - // Update internal nodes up to the root - let mut current_index = current_node_index; - while current_index > 0 { - current_index = (current_index - 1) / 2; // Index of parent node - let left_child_index = 2 * current_index + 1; - let right_child_index = 2 * current_index + 2; - - let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); - let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); - if left_child_value.1 > right_child_value.1 { - self.fsm_tree[current_index] = left_child_value - } else { - self.fsm_tree[current_index] = right_child_value - } + return Some(best_fit_offset); } - return Some(offset); + None } fn allocate_at(&mut self, size: u32, offset: u32) -> bool { - // Because the tree is sorted by offset because of how it's build, this shouldn't be to - // hard to implement efficiently - todo!() + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + + // Rebuild the tree to reflect changes. This is **not** efficient but the easiest solution, + // as the allocate_at is called only **once** on loading the bitmap from disk. + self.build_tree(); + true } } impl BestFitTree { - fn get_free_segments(&mut self) -> Vec<(u32, u32)> { + fn build_tree(&mut self) { + self.tree.clear(); + let mut offset: u32 = 0; - let mut free_segments = Vec::new(); while offset < SEGMENT_SIZE as u32 { if !self.data()[offset as usize] { // If bit is 0, it's free @@ -114,196 +82,25 @@ impl BestFitTree { current_size += 1; offset += 1; } - free_segments.push((start_offset, current_size)); + self.insert_free_segment(current_size, start_offset); } else { offset += 1; } } - free_segments } - fn build_fsm_tree(&mut self) { - let leaf_nodes = self.get_free_segments(); - let leaf_nodes_num = leaf_nodes.len(); - - if leaf_nodes_num == 0 { - self.fsm_tree = vec![(0, 0)]; // Root node with 0 free space - return; - } - - // Calculate the size of the FSM tree array. For simplicity we assume complete tree for now. - self.tree_height = (leaf_nodes_num as f64).log2().ceil() as u32; - // Number of nodes in complete binary tree of height h is 2^(h+1) - 1 - let tree_nodes_num = (1 << (self.tree_height + 1)) - 1; - - self.fsm_tree.clear(); - self.fsm_tree.resize(tree_nodes_num as usize, (0, 0)); - - // 1. Initialize leaf nodes in fsm_tree from free_segments - // OPTIM: just use memcpy - for (i, &(offset, size)) in leaf_nodes.iter().enumerate() { - // Leaf nodes are at the end of the fsm_tree array in a complete binary tree - let leaf_index = (tree_nodes_num / 2) + i; - if leaf_index < tree_nodes_num { - // Prevent out-of-bounds access if free_segments.len() is not power of 2 - self.fsm_tree[leaf_index] = (offset, size); - } - } - - // 2. Build internal nodes bottom-up similar to a binary heap - for i in (0..(tree_nodes_num / 2)).rev() { - let left_child_index = 2 * i + 1; - let right_child_index = 2 * i + 2; - - // Default to 0 if index is out of bounds (incomplete tree) - let left_child_value = *self.fsm_tree.get(left_child_index).unwrap_or(&(0, 0)); - let right_child_value = *self.fsm_tree.get(right_child_index).unwrap_or(&(0, 0)); + fn insert_free_segment(&mut self, size: u32, offset: u32) { + self.tree.entry(size).or_insert_with(Vec::new).push(offset); + } - if left_child_value.1 > right_child_value.1 { - self.fsm_tree[i] = left_child_value - } else { - self.fsm_tree[i] = right_child_value + fn remove_free_segment(&mut self, size: u32, offset: u32) { + if let Some(offsets) = self.tree.get_mut(&size) { + if let Some(index) = offsets.iter().position(|&seg_offset| seg_offset == offset) { + offsets.remove(index); + if offsets.is_empty() { + self.tree.remove(&size); + } } } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn build_empty() { - let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let allocator = BestFitTree::new(bitmap); - - // In an empty bitmap, the root node should have a large free space - assert_eq!(allocator.fsm_tree[0].0, 0 as u32); - assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32); - assert_eq!(allocator.tree_height, 0); - } - - #[test] - fn build_simple() { - // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free - let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); - let bitmap = allocator.data(); - - // Manually allocate some segments - bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning - bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones - - let mut allocator = BestFitTree::new(bitmap.into_inner()); - - let fsm_tree = vec![ - (7, SEGMENT_SIZE as u32 - 7), - (3, 2), - (7, SEGMENT_SIZE as u32 - 7), - ]; - assert_eq!(allocator.fsm_tree, fsm_tree); - assert_eq!(allocator.tree_height, 1); - } - - #[test] - fn build_complex() { - let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); - let bitmap = allocator.data(); - - // Manually allocate some segments to create a non-trivial tree - bitmap[0..3].fill(true); - bitmap[5..8].fill(true); - bitmap[8..10].fill(true); - bitmap[14..22].fill(true); - bitmap[35..36].fill(true); - bitmap[42..53].fill(true); - - let allocator = BestFitTree::new(bitmap.into_inner()); - - // binary heap layout - let fsm_tree = vec![ - (53, SEGMENT_SIZE as u32 - 53), - (22, 13), - (53, SEGMENT_SIZE as u32 - 53), - (10, 4), - (22, 13), - (53, SEGMENT_SIZE as u32 - 53), - (0, 0), - (3, 2), - (10, 4), - (22, 13), - (36, 6), - (53, SEGMENT_SIZE as u32 - 53), - (0, 0), - (0, 0), - (0, 0), - ]; - - assert_eq!(fsm_tree, allocator.fsm_tree); - assert_eq!(allocator.tree_height, 3); - } - - #[test] - fn allocate_empty_fsm_tree() { - let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let mut allocator = BestFitTree::new(bitmap); - - let allocation = allocator.allocate(1024); - assert!(allocation.is_some()); // Allocation should succeed - - let allocated_offset = allocation.unwrap(); - assert_eq!(allocated_offset, 0); // Should allocate at the beginning - - // Check if the allocated region is marked as used in the bitmap - assert!(allocator.data()[0..1024 as usize].all()); - // Check root node value after allocation - assert_eq!(allocator.fsm_tree[0], (1024, SEGMENT_SIZE as u32 - 1024)); - } - - #[test] - fn allocate_complex_fsm_tree() { - let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); - let bitmap = allocator.data(); - - // Manually allocate some segments to create a non-trivial tree - bitmap[0..3].fill(true); - bitmap[5..8].fill(true); - bitmap[8..10].fill(true); - bitmap[14..22].fill(true); - bitmap[35..36].fill(true); - bitmap[42..53].fill(true); - - let mut allocator = BestFitTree::new(bitmap.into_inner()); - - // Best-fit should allocate from the segment at offset 3 with size 2 - let allocation = allocator.allocate(2); // Request allocation of size 2 - assert!(allocation.is_some()); - assert_eq!(allocation.unwrap(), 3); - // Verify that the allocated region is marked in the bitmap - assert!(allocator.data()[3..5].all()); - - let allocation2 = allocator.allocate(10); - assert!(allocation2.is_some()); - assert_eq!(allocation2.unwrap(), 22); - assert!(allocator.data()[22..32].all()); - - // Allocate again, to use the next best fit segment - let allocation2 = allocator.allocate(100); - assert!(allocation2.is_some()); - assert_eq!(allocation2.unwrap(), 53); - assert!(allocator.data()[53..153].all()); - assert_eq!(allocator.fsm_tree[0].1, SEGMENT_SIZE as u32 - 153); - } - - #[test] - fn allocate_fail_fsm_tree() { - let mut allocator = BestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); - let root_free_space = allocator.fsm_tree[0].1; - - // Try to allocate more than available space - let allocation = allocator.allocate(root_free_space + 1); - assert!(allocation.is_none()); // Allocation should fail - - // Check if fsm_tree root value is still the same - assert_eq!(allocator.fsm_tree[0].1, root_free_space); // Should remain unchanged - } -} diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index e8a20ab24..04dbde04c 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -13,6 +13,10 @@ use serde::{Deserialize, Serialize}; // the allocator bitmap from disk) and it's the only reason, why the free_segments are kept sorted // anyway. So changing that function to respect this change could increase the performance. // +// OPTIM: For all the LIST variants of allocators: When building the bitmap we can save the size of +// the largest free segment. This could save some entire list traversals but introduces one branch +// per allocation. +// // OPTIM: For all the SCAN variants of allocators: we could use assembly (and possibly SIMD) to // count the leading/trailing 0s/1s. This could be faster than relying on the compiler. // @@ -51,6 +55,9 @@ pub use self::first_fit_tree::FirstFitTree; mod best_fit_tree; pub use self::best_fit_tree::BestFitTree; +mod approximate_best_fit_tree; +pub use self::approximate_best_fit_tree::ApproximateBestFitTree; + mod worst_fit_tree; pub use self::worst_fit_tree::WorstFitTree; @@ -77,10 +84,10 @@ pub enum AllocatorType { /// first free block that is large enough to satisfy the request. FirstFitList, - /// **First Fit FSM:** + /// **First Fit Tree:** /// This allocator builds a binary tree of offsets and sizes, that has the /// max-heap property on the sizes and uses it to find suitable free space. - FirstFitFSM, + FirstFitTree, /// **Next Fit Scan:** /// This allocator starts searching from the last allocation and continues @@ -104,10 +111,15 @@ pub enum AllocatorType { /// segment from this list to allocate memory. BestFitList, - /// **Best Fit FSM:** + /// **Best Fit Tree:** + /// This allocator builds a btree of offsets and sizes, that is sorted by + /// the sizes and uses it to find suitable free space. + BestFitTree, + + /// **Approximate Best Fit Tree:** /// This allocator builds a binary tree of offsets and sizes, that has the /// max-heap property on the sizes and uses it to find suitable free space. - BestFitFSM, + ApproximateBestFitTree, /// **Worst Fit Scan:** /// This allocator searches the entire segment and allocates the largest @@ -120,10 +132,10 @@ pub enum AllocatorType { /// (largest) segment from this list to allocate memory. WorstFitList, - /// **Worst Fit FSM:** + /// **Worst Fit Tree:** /// This allocator builds a binary tree of offsets and sizes, that has the /// max-heap property on the sizes and uses it to find suitable free space. - WorstFitFSM, + WorstFitTree, /// **Segment Allocator:** /// This is a first fit allocator that was used before making the allocators diff --git a/betree/src/allocator/worst_fit_tree.rs b/betree/src/allocator/worst_fit_tree.rs index 66b5b5174..5d41e0969 100644 --- a/betree/src/allocator/worst_fit_tree.rs +++ b/betree/src/allocator/worst_fit_tree.rs @@ -2,8 +2,6 @@ use super::*; /// Based on the free-space-map allocator from postgresql: /// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README -/// This is an approximate worst fit allocator. It will not always find the worst fit but it tries -/// its best. pub struct WorstFitTree { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 62e77e86d..06ec9c5a2 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -198,15 +198,16 @@ impl Handler { let mut allocator: Box = match self.allocator { AllocatorType::FirstFitScan => Box::new(FirstFitScan::new(bitmap)), AllocatorType::FirstFitList => Box::new(FirstFitList::new(bitmap)), - AllocatorType::FirstFitFSM => Box::new(FirstFitTree::new(bitmap)), + AllocatorType::FirstFitTree => Box::new(FirstFitTree::new(bitmap)), AllocatorType::NextFitScan => Box::new(NextFitScan::new(bitmap)), AllocatorType::NextFitList => Box::new(NextFitList::new(bitmap)), AllocatorType::BestFitScan => Box::new(BestFitScan::new(bitmap)), AllocatorType::BestFitList => Box::new(BestFitList::new(bitmap)), - AllocatorType::BestFitFSM => Box::new(BestFitTree::new(bitmap)), + AllocatorType::BestFitTree => Box::new(BestFitTree::new(bitmap)), + AllocatorType::ApproximateBestFitTree => Box::new(ApproximateBestFitTree::new(bitmap)), AllocatorType::WorstFitScan => Box::new(WorstFitScan::new(bitmap)), AllocatorType::WorstFitList => Box::new(WorstFitList::new(bitmap)), - AllocatorType::WorstFitFSM => Box::new(WorstFitTree::new(bitmap)), + AllocatorType::WorstFitTree => Box::new(WorstFitTree::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), }; From c3aaf8f9834a828f06a5d87d65ecacd81db0d451 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Mon, 17 Feb 2025 10:34:12 +0100 Subject: [PATCH 21/36] fix index error in clock_cache --- betree/src/cache/clock_cache.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/betree/src/cache/clock_cache.rs b/betree/src/cache/clock_cache.rs index db53565e1..ad8cdc4f6 100644 --- a/betree/src/cache/clock_cache.rs +++ b/betree/src/cache/clock_cache.rs @@ -267,6 +267,10 @@ impl Date: Mon, 17 Feb 2025 10:34:37 +0100 Subject: [PATCH 22/36] linting --- betree/benches/allocator.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 2e56ece88..729071f47 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -4,9 +4,11 @@ use betree_storage_stack::allocator::{ WorstFitScan, WorstFitTree, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; -use rand::distributions::{Distribution, Uniform}; -use rand::rngs::StdRng; -use rand::SeedableRng; +use rand::{ + distributions::{Distribution, Uniform}, + rngs::StdRng, + SeedableRng, +}; use zipf::ZipfDistribution; #[derive(Clone)] From cf76b2ee30a754f2b059bdfb7d04b44add6d3a93 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Mon, 17 Feb 2025 10:35:29 +0100 Subject: [PATCH 23/36] use build_threaded in c_interface --- betree/src/c_interface.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/betree/src/c_interface.rs b/betree/src/c_interface.rs index b4717b08d..f4b432d29 100644 --- a/betree/src/c_interface.rs +++ b/betree/src/c_interface.rs @@ -4,6 +4,7 @@ use std::{ env::SplitPaths, ffi::{CStr, OsStr}, io::{stderr, BufReader, Write}, + ops::Deref, os::{ raw::{c_char, c_int, c_uint, c_ulong}, unix::prelude::OsStrExt, @@ -15,9 +16,11 @@ use std::{ }; use libc::{c_void, memcpy}; +use parking_lot::RwLock; use crate::{ cow_bytes::{CowBytes, SlicedCowBytes}, + data_management::Dml, database::{AccessMode, Database, Dataset, Error, Snapshot}, object::{ObjectHandle, ObjectStore}, storage_pool::{LeafVdev, StoragePoolConfiguration, TierConfiguration, Vdev}, @@ -124,6 +127,20 @@ impl HandleResult for Database { } } +impl HandleResult for Arc> { + type Result = *mut db_t; + fn success(self) -> *mut db_t { + unsafe { + let rwlock_db = Arc::into_raw(self); + let db = rwlock_db.read(); + b(db_t(db.into_inner())) + } + } + fn fail() -> *mut db_t { + null_mut() + } +} + impl HandleResult for Dataset { type Result = *mut ds_t; fn success(self) -> *mut ds_t { @@ -378,7 +395,7 @@ pub unsafe extern "C" fn betree_configuration_set_disks( /// On error, return null. If `err` is not null, store an error in `err`. #[no_mangle] pub unsafe extern "C" fn betree_build_db(cfg: *const cfg_t, err: *mut *mut err_t) -> *mut db_t { - Database::build((*cfg).0.clone()).handle_result(err) + Database::build_threaded((*cfg).0.clone()).handle_result(err) } /// Open a database given by a configuration. If no initialized database is present this procedure will fail. @@ -389,7 +406,7 @@ pub unsafe extern "C" fn betree_build_db(cfg: *const cfg_t, err: *mut *mut err_t pub unsafe extern "C" fn betree_open_db(cfg: *const cfg_t, err: *mut *mut err_t) -> *mut db_t { let mut db_cfg = (*cfg).0.clone(); db_cfg.access_mode = AccessMode::OpenIfExists; - Database::build(db_cfg).handle_result(err) + Database::build_threaded(db_cfg).handle_result(err) } /// Create a database given by a configuration. @@ -402,7 +419,7 @@ pub unsafe extern "C" fn betree_open_db(cfg: *const cfg_t, err: *mut *mut err_t) pub unsafe extern "C" fn betree_create_db(cfg: *const cfg_t, err: *mut *mut err_t) -> *mut db_t { let mut db_cfg = (*cfg).0.clone(); db_cfg.access_mode = AccessMode::AlwaysCreateNew; - Database::build(db_cfg).handle_result(err) + Database::build_threaded(db_cfg).handle_result(err) } /// Create a database given by a configuration. @@ -418,7 +435,8 @@ pub unsafe extern "C" fn betree_open_or_create_db( ) -> *mut db_t { let mut db_cfg = (*cfg).0.clone(); db_cfg.access_mode = AccessMode::OpenOrCreate; - Database::build(db_cfg).handle_result(err) + // BUG: This returns a nullptr + Database::build_threaded(db_cfg).handle_result(err) } /// Sync a database. From 74efceaa8b8d17653ea01e898013da7a301e6580 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Mon, 17 Feb 2025 10:38:10 +0100 Subject: [PATCH 24/36] remove not improving optimizations, see branch fsm_optimization --- betree/src/allocator/mod.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 04dbde04c..894f0c11c 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -16,12 +16,6 @@ use serde::{Deserialize, Serialize}; // OPTIM: For all the LIST variants of allocators: When building the bitmap we can save the size of // the largest free segment. This could save some entire list traversals but introduces one branch // per allocation. -// -// OPTIM: For all the SCAN variants of allocators: we could use assembly (and possibly SIMD) to -// count the leading/trailing 0s/1s. This could be faster than relying on the compiler. -// -// OPTIM: For the FSM variants: we could when updating the internal nodes on insertion stop early, -// when we notice, that the offset changes mod first_fit_scan; pub use self::first_fit_scan::FirstFitScan; From 544b81dfccd6102e1d9421eaa4dbdd83bd65b8f5 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 21 Feb 2025 10:04:01 +0100 Subject: [PATCH 25/36] new plots --- scripts/visualize_allocation_log | 101 ++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/scripts/visualize_allocation_log b/scripts/visualize_allocation_log index b5bbc5064..c57de7b37 100755 --- a/scripts/visualize_allocation_log +++ b/scripts/visualize_allocation_log @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +from collections import Counter import functools from multiprocessing import Pool, Value, Lock import os @@ -503,6 +504,8 @@ class Plotter: "allocation_cycles_proportion_aligned": False, "allocation_sizes": False, "allocation_sizes_ecdf": False, # empirical cumulative distribution function + "allocation_size_by_time": False, + "cycles_by_allocation_size": False, "slider": False, "checkboxes": not args.disable_checkboxes, } @@ -549,8 +552,10 @@ class Plotter: self.vline_allocation_cycles_total_aligned = self._allocation_cycles_total_aligned() self.vline_allocation_cycles_proportion = self._allocation_cycles_proportion() self.vline_allocation_cycles_proportion_aligned = self._allocation_cycles_proportion_aligned() - _ = self._allocation_sizes() - _ = self._allocation_sizes_ecdf() + self._allocation_sizes() + self._allocation_sizes_ecdf() + self._allocation_size_by_time() + self._cycles_by_allocation_size() self.slider = self._setup_slider() self.checkboxes = self._setup_checkboxes() @@ -667,6 +672,22 @@ class Plotter: for layer in self.layers: layout[-1].append("allocation_sizes_ecdf") + if self.plot_config["allocation_size_by_time"]: + layout.append([]) + gridspec["height_ratios"].append(1) + if self.plot_config["checkboxes"]: + layout[-1].append("checkboxes") + for layer in self.layers: + layout[-1].append("allocation_size_by_time") + + if self.plot_config["cycles_by_allocation_size"]: + layout.append([]) + gridspec["height_ratios"].append(1) + if self.plot_config["checkboxes"]: + layout[-1].append("checkboxes") + for layer in self.layers: + layout[-1].append("cycles_by_allocation_size") + if self.plot_config["slider"]: layout.append([]) gridspec["height_ratios"].append(0.1) @@ -998,6 +1019,7 @@ class Plotter: failed_allocations_ax.set_title("Allocation sizes") failed_allocations_ax.set_xlim(0, max(self.global_bitmap.sizes)) failed_allocations_ax.set_ylim(0) + failed_allocations_ax.set_yscale("symlog") failed_allocations_ax.set_xlabel("Sizes") failed_allocations_ax.set_ylabel("Amount") @@ -1014,6 +1036,81 @@ class Plotter: failed_allocations_ax.set_xlabel("Sizes") failed_allocations_ax.set_ylabel("Proportion") + def _allocation_size_by_time(self): + """Plots allocation size by timestamp.""" + if not self.plot_config["allocation_size_by_time"]: + return + + data = self.global_bitmap.sizes + x_values = np.arange(len(self.global_bitmap.sizes)) + + ax = self.axd["allocation_size_by_time"] + ax.scatter(x_values, data, s=5.0, linewidths=0) + ax.set_title("Allocation Size by Timestamp") + ax.set_xlabel("Timestamp") + ax.set_ylabel("Allocation Size (Blocks)") + ax.set_ylim(1) + ax.set_xlim(0, len(x_values)) + + window_sizes = [100, 500, 1000] + colors = ['red', 'orange', 'black', 'purple'] + line_styles = ['-', '--', '-.', ':'] + moving_average = np.zeros_like(data, dtype=float) + + for i, window_size in enumerate(window_sizes): + moving_average = np.zeros_like(data, dtype=float) + for j in range(len(data)): + window_start = max(0, j - window_size + 1) + window_end = j + 1 + moving_average[j] = np.mean(data[window_start:window_end]) + + # Plot with different colors and line styles + ax.plot(x_values, moving_average, + linewidth=1.5, + color=colors[i % len(colors)], + linestyle=line_styles[i % len(line_styles)], + label=f"Moving Average ({window_size})") + + ax.legend(loc="upper left") + + def _cycles_by_allocation_size(self): + """Plots cycles by allocation size using box plots, bucketed for legibility.""" + if not self.plot_config["cycles_by_allocation_size"]: + return + + ax = self.axd["cycles_by_allocation_size"] + + # Define allocation size buckets + bucket_size = 16 + max_size = max(self.global_bitmap.sizes) + buckets = range(0, max_size + bucket_size, bucket_size) + bucket_begins = [b for b in buckets] # begins of the buckets for boxplot positions + + # Initialize data structures for bucketed cycles + bucketed_cycles = {begin: [] for begin in bucket_begins} + + # Bucket cycles by allocation size + for size, cycles in zip(self.global_bitmap.sizes, self.global_bitmap.cycles_alloc[1]): + if size <= max_size: + for begin in bucket_begins: + if buckets[bucket_begins.index(begin)] <= size < buckets[bucket_begins.index(begin)] + bucket_size: + bucketed_cycles[begin].append(cycles) + break + + # Prepare data for boxplot: list of cycle lists for each bucket + boxplot_data = [bucketed_cycles[begin] for begin in bucket_begins if bucketed_cycles[begin]] + positions = [begin for begin in bucket_begins if bucketed_cycles[begin]] # Positions of the boxplots are bucket begins + + # Plotting box plots + ax.boxplot(boxplot_data, positions=positions, widths=bucket_size*0.5, showfliers=True, manage_ticks=False) + + ax.set_title("Cycles by Allocation Size (Box Plot, Bucketed)") + ax.set_xlabel("Allocation Size (Blocks)") + ax.set_ylabel("Allocation Cycles") + ax.set_xlim(0, max_size + bucket_size) + ax.set_xticks(range(0, max_size + bucket_size + 1, 64)) + ax.set_ylim(0) + def _setup_slider(self): """Helper method for setting up the slider for interactive plotting.""" if not self.plot_config["slider"]: From 3b4a02f4434fb3aa6fb57d0f0cb7afc82db9960c Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Wed, 26 Feb 2025 20:06:15 +0100 Subject: [PATCH 26/36] hybrid allocator --- betree/benches/allocator.rs | 7 +- betree/src/allocator/hybrid_allocator.rs | 144 +++++++++++++++++++++++ betree/src/allocator/mod.rs | 19 +-- betree/src/database/handler.rs | 1 + 4 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 betree/src/allocator/hybrid_allocator.rs diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 729071f47..41be120fd 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,7 +1,7 @@ use betree_storage_stack::allocator::{ self, Allocator, ApproximateBestFitTree, BestFitList, BestFitScan, BestFitTree, FirstFitList, FirstFitScan, FirstFitTree, NextFitList, NextFitScan, SegmentAllocator, WorstFitList, - WorstFitScan, WorstFitTree, SEGMENT_SIZE_BYTES, + WorstFitScan, WorstFitTree, HybridAllocator, SEGMENT_SIZE_BYTES, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::{ @@ -120,8 +120,8 @@ fn bench_allocator_with_sync( } pub fn criterion_benchmark(c: &mut Criterion) { - let min_size = 64; - let max_size = 4096; + let min_size = 128; + let max_size = 1024; let zipfian_exponent = 0.99; let distributions = [ @@ -154,6 +154,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { Box::new(AllocatorBenchmark::::new("worst_fit_list")), Box::new(AllocatorBenchmark::::new("worst_fit_tree")), Box::new(AllocatorBenchmark::::new("segment")), + Box::new(AllocatorBenchmark::::new("hybrid")), ]; let alloc_dealloc_ratios = [ diff --git a/betree/src/allocator/hybrid_allocator.rs b/betree/src/allocator/hybrid_allocator.rs new file mode 100644 index 000000000..ba1c41385 --- /dev/null +++ b/betree/src/allocator/hybrid_allocator.rs @@ -0,0 +1,144 @@ +use super::*; + +const POOL_PERCENTAGE: f64 = 0.75; +const SECTION_SIZE: usize = 768; + +// Only tentative, because this is not guaranteed to be a multiple of `SECTION_SIZE`. +const POOL_BLOCKS_TENTATIVE: usize = (SEGMENT_SIZE as f64 * POOL_PERCENTAGE) as usize; + +const POOL_ELEMENTS: usize = POOL_BLOCKS_TENTATIVE / SECTION_SIZE; +const POOL_BITMAP_BYTES: usize = POOL_ELEMENTS / 8; +const POOL_BLOCKS: usize = POOL_ELEMENTS * SECTION_SIZE; + +/// Hybrid allocator with a pool for `SECTION_SIZE`d-block allocations and NextFitScan for the rest. +pub struct HybridAllocator { + // Underlying bitmap of the allocator. The NextFitScan allocator works directly on this bitmap. + data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + // Bitmap for the pool, one bit manages one section + pool_bitmap: BitArr!(for POOL_ELEMENTS, in u8, Lsb0), + last_offset_pool: usize, + last_offset: u32, +} + +impl Allocator for HybridAllocator { + fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { + &mut self.data + } + + fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self + where + Self: Sized, + { + let mut allocator = HybridAllocator { + data: BitArray::new(bitmap), + pool_bitmap: BitArray::new([0u8; POOL_BITMAP_BYTES]), + last_offset: POOL_BLOCKS as u32, + last_offset_pool: 0, + }; + allocator.initialize_pool(); + allocator + } + + fn allocate(&mut self, size: u32) -> Option { + if size == 0 { + return Some(0); + } + + if size == SECTION_SIZE as u32 { + // Try to allocate from the pool using Next-Fit within the pool. + for _ in 0..POOL_ELEMENTS { + // Iterate through all pool elements for Next-Fit + if self.last_offset_pool >= POOL_ELEMENTS { + self.last_offset_pool = 0; // Wrap around + } + + let pool_offset_index = self.last_offset_pool; + + if !self.pool_bitmap[pool_offset_index] { + self.pool_bitmap.set(pool_offset_index, true); + let offset = (pool_offset_index * SECTION_SIZE) as u32; + self.mark(offset, size, Action::Allocate); + self.last_offset_pool += 1; + return Some(offset); + } + + self.last_offset_pool += 1; + } + } + + // Fallback to next-fit allocator for other sizes and if no space found. + let mut offset = self.last_offset; + let mut wrap_around = false; + + loop { + if offset + size > SEGMENT_SIZE as u32 { + // Wrap around to the beginning of the segment. + // NOTE: We **can't** set offset here. + wrap_around = true; + } + + if wrap_around && offset >= self.last_offset { + // We've circled back to the starting point, no space found + return None; + } + + if offset + size > SEGMENT_SIZE as u32 { + // NOTE: We **can't** set offset above because of the case if self.last_offset is + // larger than SEGMENT_SIZE - size. If it is and we would set offset above we would + // run into an infinite loop. Because the `offset >= self.last_offset` condition + // would never be fulfilled because offset is reset beforehand. + offset = POOL_BLOCKS as u32; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + + match self.data[start_idx..end_idx].last_one() { + Some(last_alloc_idx) => { + // Skip to the end of the last allocated block if there is any one at all. + offset += last_alloc_idx as u32 + 1 + } + None => { + // No allocated blocks found, so allocate here. + self.mark(offset, size, Action::Allocate); + self.last_offset = offset + size + 1; + return Some(offset); + } + } + } + } + + fn allocate_at(&mut self, size: u32, offset: u32) -> bool { + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + + // Mark the correct bit in the pool as allocated if the offset is in the pool. + if offset < POOL_BLOCKS as u32 { + self.pool_bitmap.set(offset as usize / SECTION_SIZE, true); + } + + true + } +} + +impl HybridAllocator { + fn initialize_pool(&mut self) { + // Initialize the pool bitmap + for i in 0..POOL_ELEMENTS { + if self.data[i * SECTION_SIZE..(i + 1) * SECTION_SIZE].any() { + self.pool_bitmap.set(i, true); + } + } + } +} diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 894f0c11c..38ea7c9fc 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -3,19 +3,6 @@ use bitvec::prelude::*; use byteorder::{BigEndian, ByteOrder}; use serde::{Deserialize, Serialize}; -// OPTIM: For all the LIST variants of allocators: try to remove a free segment that can be created -// on allocation. There are two ways: -// 1. call remove on free_segments: this copies all the elements after the removed one, one place -// to the front. This keeps the sorting of the array -// 2. swap remove the segment: swap the empty segment to the back and decrease the len by 1. This -// breaks the sorting but is probably much faster -// The allocate_at function is called quite rarely (only on creation of the database and on loading -// the allocator bitmap from disk) and it's the only reason, why the free_segments are kept sorted -// anyway. So changing that function to respect this change could increase the performance. -// -// OPTIM: For all the LIST variants of allocators: When building the bitmap we can save the size of -// the largest free segment. This could save some entire list traversals but introduces one branch -// per allocation. mod first_fit_scan; pub use self::first_fit_scan::FirstFitScan; @@ -55,6 +42,9 @@ pub use self::approximate_best_fit_tree::ApproximateBestFitTree; mod worst_fit_tree; pub use self::worst_fit_tree::WorstFitTree; +mod hybrid_allocator; +pub use self::hybrid_allocator::HybridAllocator; + /// 256KiB, so that `vdev::BLOCK_SIZE * SEGMENT_SIZE == 1GiB` pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap @@ -135,6 +125,9 @@ pub enum AllocatorType { /// This is a first fit allocator that was used before making the allocators /// generic. It is not efficient and mainly included for reference. SegmentAllocator, + + /// **Hybrid Allocator:** + HybridAllocator, } /// The `Allocator` trait defines an interface for allocating and deallocating diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index 06ec9c5a2..b1c23cef3 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -209,6 +209,7 @@ impl Handler { AllocatorType::WorstFitList => Box::new(WorstFitList::new(bitmap)), AllocatorType::WorstFitTree => Box::new(WorstFitTree::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), + AllocatorType::HybridAllocator => Box::new(HybridAllocator::new(bitmap)), }; if let Some((offset, size)) = self.old_root_allocation.read() { From e91a3c0a3033c4450343d03197ce5ff50c4c378f Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Mon, 3 Mar 2025 12:13:46 +0100 Subject: [PATCH 27/36] small refactor --- betree/src/allocator/hybrid_allocator.rs | 95 +++++++++++++++--------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/betree/src/allocator/hybrid_allocator.rs b/betree/src/allocator/hybrid_allocator.rs index ba1c41385..f776682f2 100644 --- a/betree/src/allocator/hybrid_allocator.rs +++ b/betree/src/allocator/hybrid_allocator.rs @@ -10,14 +10,59 @@ const POOL_ELEMENTS: usize = POOL_BLOCKS_TENTATIVE / SECTION_SIZE; const POOL_BITMAP_BYTES: usize = POOL_ELEMENTS / 8; const POOL_BLOCKS: usize = POOL_ELEMENTS * SECTION_SIZE; +struct Pool { + bitmap: BitArray<[u8; BYTES], Lsb0>, + last_offset: usize, +} + +impl + Pool +{ + fn new(bitmap: BitArr!(for SEGMENT_SIZE, in u8, Lsb0)) -> Self { + let mut pool = Pool { + bitmap: BitArray::new([0u8; BYTES]), + last_offset: 0, + }; + + // Initialize the pool bitmap + for i in 0..ELEMENTS { + let start = i * SECTION_SIZE; + let end = (i + 1) * SECTION_SIZE; + if bitmap[start..end].any() { + pool.bitmap.set(i, true); + } + } + + pool + } + + fn allocate_section(&mut self) -> Option { + // Next-Fit allocation within the pool + for _ in 0..ELEMENTS { + if self.last_offset >= ELEMENTS { + self.last_offset = 0; // Wrap around + } + + let offset = self.last_offset; + self.last_offset += 1; + + if !self.bitmap[offset] { + // Found free space. + self.bitmap.set(offset, true); + return Some(offset as u32); + } + } + None + } +} + /// Hybrid allocator with a pool for `SECTION_SIZE`d-block allocations and NextFitScan for the rest. pub struct HybridAllocator { // Underlying bitmap of the allocator. The NextFitScan allocator works directly on this bitmap. data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), - // Bitmap for the pool, one bit manages one section - pool_bitmap: BitArr!(for POOL_ELEMENTS, in u8, Lsb0), - last_offset_pool: usize, last_offset: u32, + // Bitmap for the pool, one bit manages one section + pool: Pool, } impl Allocator for HybridAllocator { @@ -29,13 +74,12 @@ impl Allocator for HybridAllocator { where Self: Sized, { + let data = BitArray::new(bitmap); let mut allocator = HybridAllocator { - data: BitArray::new(bitmap), - pool_bitmap: BitArray::new([0u8; POOL_BITMAP_BYTES]), + data, last_offset: POOL_BLOCKS as u32, - last_offset_pool: 0, + pool: Pool::new(data), }; - allocator.initialize_pool(); allocator } @@ -45,24 +89,12 @@ impl Allocator for HybridAllocator { } if size == SECTION_SIZE as u32 { - // Try to allocate from the pool using Next-Fit within the pool. - for _ in 0..POOL_ELEMENTS { - // Iterate through all pool elements for Next-Fit - if self.last_offset_pool >= POOL_ELEMENTS { - self.last_offset_pool = 0; // Wrap around - } - - let pool_offset_index = self.last_offset_pool; - - if !self.pool_bitmap[pool_offset_index] { - self.pool_bitmap.set(pool_offset_index, true); - let offset = (pool_offset_index * SECTION_SIZE) as u32; - self.mark(offset, size, Action::Allocate); - self.last_offset_pool += 1; - return Some(offset); - } - - self.last_offset_pool += 1; + // Try to allocate from the pool. + if let Some(pool_offset) = self.pool.allocate_section() { + // Found free space. + let offset = pool_offset * SEGMENT_SIZE as u32; + self.mark(offset, size, Action::Allocate); + return Some(offset); } } @@ -125,20 +157,9 @@ impl Allocator for HybridAllocator { // Mark the correct bit in the pool as allocated if the offset is in the pool. if offset < POOL_BLOCKS as u32 { - self.pool_bitmap.set(offset as usize / SECTION_SIZE, true); + self.pool.bitmap.set(offset as usize / SECTION_SIZE, true); } true } } - -impl HybridAllocator { - fn initialize_pool(&mut self) { - // Initialize the pool bitmap - for i in 0..POOL_ELEMENTS { - if self.data[i * SECTION_SIZE..(i + 1) * SECTION_SIZE].any() { - self.pool_bitmap.set(i, true); - } - } - } -} From a04ee99affe148d13dbc5996acb9469812dc47d2 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Mon, 3 Mar 2025 14:26:24 +0100 Subject: [PATCH 28/36] support multiple pools --- betree/src/allocator/hybrid_allocator.rs | 175 ++++++++++++++++------- 1 file changed, 124 insertions(+), 51 deletions(-) diff --git a/betree/src/allocator/hybrid_allocator.rs b/betree/src/allocator/hybrid_allocator.rs index f776682f2..3aa8ad1fc 100644 --- a/betree/src/allocator/hybrid_allocator.rs +++ b/betree/src/allocator/hybrid_allocator.rs @@ -1,45 +1,97 @@ use super::*; -const POOL_PERCENTAGE: f64 = 0.75; -const SECTION_SIZE: usize = 768; - -// Only tentative, because this is not guaranteed to be a multiple of `SECTION_SIZE`. -const POOL_BLOCKS_TENTATIVE: usize = (SEGMENT_SIZE as f64 * POOL_PERCENTAGE) as usize; - -const POOL_ELEMENTS: usize = POOL_BLOCKS_TENTATIVE / SECTION_SIZE; -const POOL_BITMAP_BYTES: usize = POOL_ELEMENTS / 8; -const POOL_BLOCKS: usize = POOL_ELEMENTS * SECTION_SIZE; +// Define pool configurations at compile time. +// Each tuple represents a pool: (SECTION_SIZE, POOL_PERCENTAGE) +// NOTE: Unfortunately Rust cannot infer the number of array elements, so change that, when adding +// or removing pools. +const POOL_CONFIGS: [(usize, f64); 3] = [(768, 0.80), (128, 0.05), (256, 0.05)]; +const NUM_POOLS: usize = POOL_CONFIGS.len(); + +// Number elements/slots a pool has. +const POOL_ELEMENTS: [usize; NUM_POOLS] = { + let mut arr = [0usize; NUM_POOLS]; + let mut i = 0; + while i < NUM_POOLS { + let tentative_blocks = (SEGMENT_SIZE as f64 * POOL_CONFIGS[i].1) as usize; + arr[i] = tentative_blocks / POOL_CONFIGS[i].0; + i += 1; + } + arr +}; + +// Number of 4KiB blocks each pool manages. +const POOL_BLOCKS_PER_POOL: [usize; NUM_POOLS] = { + let mut arr = [0usize; NUM_POOLS]; + let mut i = 0; + while i < NUM_POOLS { + arr[i] = POOL_ELEMENTS[i] * POOL_CONFIGS[i].0; + i += 1; + } + arr +}; + +// Sum of POOL_BLOCKS_PER_POOL. +const POOL_BLOCKS: usize = { + let mut total_blocks = 0; + let mut i = 0; + while i < NUM_POOLS { + total_blocks += POOL_BLOCKS_PER_POOL[i]; + i += 1; + } + total_blocks +}; + +// Offset where the blocks, that a pool manage, start in the global bitmap. +const POOL_OFFSET_START: [usize; NUM_POOLS] = { + let mut arr = [0usize; NUM_POOLS]; + let mut current_offset = 0; + let mut i = 0; + while i < NUM_POOLS { + arr[i] = current_offset; + current_offset += POOL_BLOCKS_PER_POOL[i]; + i += 1; + } + arr +}; -struct Pool { - bitmap: BitArray<[u8; BYTES], Lsb0>, +struct Pool { + bitmap: BitVec, last_offset: usize, + section_size: usize, + elements: usize, } -impl - Pool -{ - fn new(bitmap: BitArr!(for SEGMENT_SIZE, in u8, Lsb0)) -> Self { - let mut pool = Pool { - bitmap: BitArray::new([0u8; BYTES]), +impl Pool { + fn new(section_size: usize, elements: usize) -> Self { + let bytes = (elements + 7) / 8; + Pool { + bitmap: BitVec::with_capacity(elements), // Initialize with capacity for performance last_offset: 0, - }; + section_size, + elements, + } + } - // Initialize the pool bitmap - for i in 0..ELEMENTS { - let start = i * SECTION_SIZE; - let end = (i + 1) * SECTION_SIZE; - if bitmap[start..end].any() { - pool.bitmap.set(i, true); + fn initialize_bitmap( + &mut self, + global_bitmap: &BitArr!(for SEGMENT_SIZE, in u8, Lsb0), + pool_start_offset: usize, + ) { + self.bitmap.resize(self.elements, false); // Actually create the bits now + // Initialize the pool bitmap based on the global bitmap + for i in 0..self.elements { + let start = pool_start_offset + i * self.section_size; + let end = pool_start_offset + (i + 1) * self.section_size; + if global_bitmap[start..end].any() { + self.bitmap.set(i, true); } } - - pool } fn allocate_section(&mut self) -> Option { // Next-Fit allocation within the pool - for _ in 0..ELEMENTS { - if self.last_offset >= ELEMENTS { + for _ in 0..self.elements { + if self.last_offset >= self.elements { self.last_offset = 0; // Wrap around } @@ -56,13 +108,13 @@ impl } } -/// Hybrid allocator with a pool for `SECTION_SIZE`d-block allocations and NextFitScan for the rest. +/// Hybrid allocator with pools for different sized-block allocations and NextFitScan for the rest. pub struct HybridAllocator { // Underlying bitmap of the allocator. The NextFitScan allocator works directly on this bitmap. data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), last_offset: u32, - // Bitmap for the pool, one bit manages one section - pool: Pool, + // Pools for fixed-size allocations, using a vector of Pools + pools: [Pool; NUM_POOLS], } impl Allocator for HybridAllocator { @@ -77,9 +129,22 @@ impl Allocator for HybridAllocator { let data = BitArray::new(bitmap); let mut allocator = HybridAllocator { data, - last_offset: POOL_BLOCKS as u32, - pool: Pool::new(data), + last_offset: POOL_BLOCKS as u32, // Next-fit starts after pools + pools: { + core::array::from_fn(|i| { + let section_size = POOL_CONFIGS[i].0; + let elements = POOL_ELEMENTS[i]; + Pool::new(section_size, elements) + }) + }, }; + + // Initialize pool bitmaps + for i in 0..NUM_POOLS { + let start_offset = POOL_OFFSET_START[i]; + allocator.pools[i].initialize_bitmap(&allocator.data, start_offset); + } + allocator } @@ -88,24 +153,29 @@ impl Allocator for HybridAllocator { return Some(0); } - if size == SECTION_SIZE as u32 { - // Try to allocate from the pool. - if let Some(pool_offset) = self.pool.allocate_section() { - // Found free space. - let offset = pool_offset * SEGMENT_SIZE as u32; - self.mark(offset, size, Action::Allocate); - return Some(offset); + for i in 0..NUM_POOLS { + if size as usize == self.pools[i].section_size { + // Try to allocate from the pool. + if let Some(pool_offset) = self.pools[i].allocate_section() { + // Found free space in pool. + let offset = (POOL_OFFSET_START[i] as u32 + + pool_offset * self.pools[i].section_size as u32) + as u32; + self.mark(offset, size, Action::Allocate); + return Some(offset); + } + // Pool is full, break and use fallback. + break; // Only check one pool if size matches. } } - // Fallback to next-fit allocator for other sizes and if no space found. + // Fallback to next-fit allocator for other sizes and if no space found in pools. let mut offset = self.last_offset; let mut wrap_around = false; loop { if offset + size > SEGMENT_SIZE as u32 { // Wrap around to the beginning of the segment. - // NOTE: We **can't** set offset here. wrap_around = true; } @@ -115,11 +185,7 @@ impl Allocator for HybridAllocator { } if offset + size > SEGMENT_SIZE as u32 { - // NOTE: We **can't** set offset above because of the case if self.last_offset is - // larger than SEGMENT_SIZE - size. If it is and we would set offset above we would - // run into an infinite loop. Because the `offset >= self.last_offset` condition - // would never be fulfilled because offset is reset beforehand. - offset = POOL_BLOCKS as u32; + offset = POOL_BLOCKS as u32; // Start next-fit scan after pools } let start_idx = offset as usize; @@ -133,7 +199,7 @@ impl Allocator for HybridAllocator { None => { // No allocated blocks found, so allocate here. self.mark(offset, size, Action::Allocate); - self.last_offset = offset + size + 1; + self.last_offset = offset + size; return Some(offset); } } @@ -155,9 +221,16 @@ impl Allocator for HybridAllocator { } self.mark(offset, size, Action::Allocate); - // Mark the correct bit in the pool as allocated if the offset is in the pool. - if offset < POOL_BLOCKS as u32 { - self.pool.bitmap.set(offset as usize / SECTION_SIZE, true); + // TODO: right now this isn't correct if the space lies in multiple sections of a pool. + for i in 0..NUM_POOLS { + let pool = &self.pools[i]; + let pool_start = POOL_OFFSET_START[i] as u32; + let pool_end = pool_start + POOL_BLOCKS_PER_POOL[i] as u32; + if offset >= pool_start && offset < pool_end { + let pool_section_offset = (offset - pool_start) / pool.section_size as u32; + self.pools[i].bitmap.set(pool_section_offset as usize, true); + return true; + } } true From a800ec73cbd0d4ddbda8589af3fc6509cfbfb7f3 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Wed, 5 Mar 2025 15:01:58 +0100 Subject: [PATCH 29/36] proper allocate_at --- betree/src/allocator/hybrid_allocator.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/betree/src/allocator/hybrid_allocator.rs b/betree/src/allocator/hybrid_allocator.rs index 3aa8ad1fc..fbbfd3346 100644 --- a/betree/src/allocator/hybrid_allocator.rs +++ b/betree/src/allocator/hybrid_allocator.rs @@ -1,4 +1,5 @@ use super::*; +use std::cmp::min; // Define pool configurations at compile time. // Each tuple represents a pool: (SECTION_SIZE, POOL_PERCENTAGE) @@ -221,15 +222,20 @@ impl Allocator for HybridAllocator { } self.mark(offset, size, Action::Allocate); - // TODO: right now this isn't correct if the space lies in multiple sections of a pool. + // Find all pools and elements of these pools, that intersect the allocation in some way + // and mark them as allocated. for i in 0..NUM_POOLS { - let pool = &self.pools[i]; + let pool = &mut self.pools[i]; let pool_start = POOL_OFFSET_START[i] as u32; let pool_end = pool_start + POOL_BLOCKS_PER_POOL[i] as u32; if offset >= pool_start && offset < pool_end { - let pool_section_offset = (offset - pool_start) / pool.section_size as u32; - self.pools[i].bitmap.set(pool_section_offset as usize, true); - return true; + let pool_section_offset = (offset - pool_start) as usize / pool.section_size; + let pool_section_size = (size as usize / pool.section_size) + 1; + + let pool_section_end = + min(pool_section_size + pool_section_offset, pool.bitmap.len()); + + pool.bitmap[pool_section_offset..pool_section_size].fill(true); } } From 0b95217e9d99f5460a82d95782943acc798dfb8f Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 7 Mar 2025 16:05:41 +0100 Subject: [PATCH 30/36] rework allocator micro benchmarks --- betree/benches/allocator.rs | 304 +++++++++++++++++++++++------------- 1 file changed, 194 insertions(+), 110 deletions(-) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 41be120fd..618b8cb07 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,7 +1,9 @@ +use std::time::{Duration, Instant}; + use betree_storage_stack::allocator::{ self, Allocator, ApproximateBestFitTree, BestFitList, BestFitScan, BestFitTree, FirstFitList, - FirstFitScan, FirstFitTree, NextFitList, NextFitScan, SegmentAllocator, WorstFitList, - WorstFitScan, WorstFitTree, HybridAllocator, SEGMENT_SIZE_BYTES, + FirstFitScan, FirstFitTree, HybridAllocator, NextFitList, NextFitScan, SegmentAllocator, + WorstFitList, WorstFitScan, WorstFitTree, SEGMENT_SIZE_BYTES, SEGMENT_SIZE_LOG_2, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::{ @@ -17,51 +19,37 @@ enum SizeDistribution { Zipfian(ZipfDistribution), } -// Define a trait for our benchmark struct to allow for trait objects -trait GenericAllocatorBenchmark { - fn benchmark_name(&self) -> &'static str; - - fn bench_allocator_with_sync( - &self, - b: &mut Bencher, - dist: SizeDistribution, - allocations: u64, - deallocations: u64, - min_size: usize, - max_size: usize, - ); -} +// Define a type alias for our benchmark function to make it less verbose +type BenchmarkFn = Box; -struct AllocatorBenchmark { - allocator_type: std::marker::PhantomData, - benchmark_name: &'static str, -} - -impl AllocatorBenchmark { - fn new(benchmark_name: &'static str) -> Self { - AllocatorBenchmark { - allocator_type: std::marker::PhantomData, - benchmark_name, - } - } +// Macro to generate allocator benchmark entries +macro_rules! allocator_benchmark { + ($name:expr, $allocator_type:ty, $bench_function:ident) => { + ( + $name, + Box::new(|b, dist, allocations, deallocations, min_size, max_size| { + $bench_function::<$allocator_type>( + b, + dist, + allocations, + deallocations, + min_size, + max_size, + ) + }), + ) + }; } -impl GenericAllocatorBenchmark for AllocatorBenchmark { - fn benchmark_name(&self) -> &'static str { - self.benchmark_name - } - - fn bench_allocator_with_sync( - &self, - b: &mut Bencher, - dist: SizeDistribution, - allocations: u64, - deallocations: u64, - min_size: usize, - max_size: usize, - ) { - bench_allocator_with_sync::(b, dist, allocations, deallocations, min_size, max_size) - } +// Macro to generate allocator benchmark entries with name derived from type +macro_rules! generate_allocator_benchmarks { + ($bench_function:ident, $($allocator_type:ty),*) => { + vec![ + $( + allocator_benchmark!(stringify!($allocator_type), $allocator_type, $bench_function), + )* + ] + }; } // In Haura, allocators are not continuously active in memory. Instead, they are loaded from disk @@ -69,7 +57,7 @@ impl GenericAllocatorBenchmark for AllocatorBenchmark // iteration. Also deallocations are buffered and applied during sync operations, not immediately to // the allocator. Here, we simulate the sync operation by directly modifying the underlying bitmap // data after the allocator has performed allocations, mimicking the delayed deallocation process. -fn bench_allocator_with_sync( +fn bench_alloc( b: &mut Bencher, dist: SizeDistribution, allocations: u64, @@ -85,37 +73,105 @@ fn bench_allocator_with_sync( match &dist { SizeDistribution::Uniform(u) => return black_box(u.sample(&mut rng)) as u32, SizeDistribution::Zipfian(z) => { - return (black_box(z.sample(&mut rng)) as usize).clamp(min_size, max_size) as u32 + let rank = black_box(z.sample(&mut rng)) as usize; + return (min_size + (rank - 1)) as u32; } } }; - b.iter(|| { - let mut allocator = A::new(data); - for _ in 0..allocations { - let size = sample_size(); - if let Some(offset) = black_box(allocator.allocate(size)) { - allocated.push((offset, size)); + b.iter_custom(|iters| { + let mut total_allocation_time = Duration::new(0, 0); + + for _ in 0..iters { + let mut allocator = A::new(data); + + let start = Instant::now(); + for _ in 0..allocations { + let size = sample_size(); + if let Some(offset) = black_box(allocator.allocate(size)) { + allocated.push((offset, size)); + } + } + total_allocation_time += start.elapsed(); + + // Simulates the deferred deallocations + let bitmap = allocator.data(); + for _ in 0..deallocations { + if allocated.is_empty() { + break; + } + let idx = rand::random::() % allocated.len(); + let (offset, size) = allocated.swap_remove(idx); + + let start = offset as usize; + let end = (offset + size) as usize; + let range = &mut bitmap[start..end]; + range.fill(false); + } + // At the end of the iteration, the allocator goes out of scope, simulating it being + // unloaded from memory. In the next iteration, a new allocator will be created and loaded + // with the modified bitmap data. + } + Duration::from_nanos((total_allocation_time.as_nanos() / allocations as u128) as u64) + }); +} + +fn bench_new( + b: &mut Bencher, + dist: SizeDistribution, + allocations: u64, + deallocations: u64, + min_size: usize, + max_size: usize, +) { + let data = [0; SEGMENT_SIZE_BYTES]; + let mut allocated = Vec::new(); + + let mut rng = StdRng::seed_from_u64(42); + let mut sample_size = || -> u32 { + match &dist { + SizeDistribution::Uniform(u) => return black_box(u.sample(&mut rng)) as u32, + SizeDistribution::Zipfian(z) => { + let rank = black_box(z.sample(&mut rng)) as usize; + return (min_size + (rank - 1)) as u32; // Linear mapping for Zipfian } } + }; - // Simulates the deferred deallocations - let bitmap = allocator.data(); - for _ in 0..deallocations { - if allocated.is_empty() { - break; + b.iter_custom(|iters| { + let mut total_allocation_time = Duration::new(0, 0); + + for _ in 0..iters { + let start = Instant::now(); + let mut allocator = A::new(data); + total_allocation_time += start.elapsed(); + + for _ in 0..allocations { + let size = sample_size(); + if let Some(offset) = black_box(allocator.allocate(size)) { + allocated.push((offset, size)); + } } - let idx = rand::random::() % allocated.len(); - let (offset, size) = allocated.swap_remove(idx); - let start = offset as usize; - let end = (offset + size) as usize; - let range = &mut bitmap[start..end]; - range.fill(false); + // Simulates the deferred deallocations + let bitmap = allocator.data(); + for _ in 0..deallocations { + if allocated.is_empty() { + break; + } + let idx = rand::random::() % allocated.len(); + let (offset, size) = allocated.swap_remove(idx); + + let start = offset as usize; + let end = (offset + size) as usize; + let range = &mut bitmap[start..end]; + range.fill(false); + } + // At the end of the iteration, the allocator goes out of scope, simulating it being + // unloaded from memory. In the next iteration, a new allocator will be created and loaded + // with the modified bitmap data. } - // At the end of the iteration, the allocator goes out of scope, simulating it being - // unloaded from memory. In the next iteration, a new allocator will be created and loaded - // with the modified bitmap data. + total_allocation_time }); } @@ -132,57 +188,85 @@ pub fn criterion_benchmark(c: &mut Criterion) { ( "zipfian", SizeDistribution::Zipfian( - ZipfDistribution::new(max_size - min_size, zipfian_exponent).expect(""), + ZipfDistribution::new(max_size - min_size + 1, zipfian_exponent).expect(""), ), ), ]; - // Define the allocators to benchmark - let allocator_benchmarks: Vec> = vec![ - Box::new(AllocatorBenchmark::::new("first_fit_scan")), - Box::new(AllocatorBenchmark::::new("first_fit_list")), - Box::new(AllocatorBenchmark::::new("first_fit_tree")), - Box::new(AllocatorBenchmark::::new("next_fit_scan")), - Box::new(AllocatorBenchmark::::new("next_fit_list")), - Box::new(AllocatorBenchmark::::new("best_fit_scan")), - Box::new(AllocatorBenchmark::::new("best_fit_list")), - Box::new(AllocatorBenchmark::::new( - "approximate_best_fit_tree", - )), - Box::new(AllocatorBenchmark::::new("best_fit_tree")), - Box::new(AllocatorBenchmark::::new("worst_fit_scan")), - Box::new(AllocatorBenchmark::::new("worst_fit_list")), - Box::new(AllocatorBenchmark::::new("worst_fit_tree")), - Box::new(AllocatorBenchmark::::new("segment")), - Box::new(AllocatorBenchmark::::new("hybrid")), - ]; + let allocations = 2_u64.pow(SEGMENT_SIZE_LOG_2 as u32 - 10); + let deallocations = allocations / 2; - let alloc_dealloc_ratios = [ - (100, 50), - (500, 250), - (1000, 500), - (5000, 2500), - (10000, 5000), - ]; + // Define the allocators to benchmark for allocation + #[rustfmt::skip] + let allocator_benchmarks_alloc: Vec<(&'static str, BenchmarkFn)> = generate_allocator_benchmarks!( + bench_alloc, + FirstFitScan, + FirstFitList, + FirstFitTree, + NextFitScan, + NextFitList, + BestFitScan, + BestFitList, + ApproximateBestFitTree, + BestFitTree, + WorstFitScan, + WorstFitList, + WorstFitTree, + SegmentAllocator + ); - for (dist_name, dist) in distributions { - for (allocations, deallocations) in alloc_dealloc_ratios { - let group_name = format!("{}_sync_{}_{}", dist_name, allocations, deallocations); - let mut group = c.benchmark_group(group_name); - for allocator_bench in &allocator_benchmarks { - group.bench_function(allocator_bench.benchmark_name(), |b| { - allocator_bench.bench_allocator_with_sync( - b, - dist.clone(), - allocations, - deallocations, - min_size, - max_size, - ) - }); - } - group.finish(); + for (dist_name, dist) in distributions.clone() { + let group_name = format!("allocator_alloc_{}_{}", dist_name, SEGMENT_SIZE_LOG_2); + let mut group = c.benchmark_group(group_name); + for (bench_name, bench_func) in &allocator_benchmarks_alloc { + group.bench_function(*bench_name, |b| { + bench_func( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); + } + group.finish(); + } + + // Define the allocators to benchmark for 'new' function time + let allocator_benchmarks_new: Vec<(&'static str, BenchmarkFn)> = generate_allocator_benchmarks!( + bench_new, + FirstFitScan, + FirstFitList, + FirstFitTree, + NextFitScan, + NextFitList, + BestFitScan, + BestFitList, + ApproximateBestFitTree, + BestFitTree, + WorstFitScan, + WorstFitList, + WorstFitTree, + SegmentAllocator + ); + + for (dist_name, dist) in distributions.clone() { + let group_name = format!("allocator_new_{}_{}", dist_name, SEGMENT_SIZE_LOG_2); + let mut group = c.benchmark_group(group_name); + for (bench_name, bench_func) in &allocator_benchmarks_new { + group.bench_function(*bench_name, |b| { + bench_func( + b, + dist.clone(), + allocations, + deallocations, + min_size, + max_size, + ) + }); } + group.finish(); } } From ebff5acf179da96f2bf294e639846656f71353b3 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 7 Mar 2025 16:07:08 +0100 Subject: [PATCH 31/36] fix small bug --- betree/src/allocator/hybrid_allocator.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/betree/src/allocator/hybrid_allocator.rs b/betree/src/allocator/hybrid_allocator.rs index fbbfd3346..3e87253e4 100644 --- a/betree/src/allocator/hybrid_allocator.rs +++ b/betree/src/allocator/hybrid_allocator.rs @@ -64,7 +64,6 @@ struct Pool { impl Pool { fn new(section_size: usize, elements: usize) -> Self { - let bytes = (elements + 7) / 8; Pool { bitmap: BitVec::with_capacity(elements), // Initialize with capacity for performance last_offset: 0, @@ -235,7 +234,7 @@ impl Allocator for HybridAllocator { let pool_section_end = min(pool_section_size + pool_section_offset, pool.bitmap.len()); - pool.bitmap[pool_section_offset..pool_section_size].fill(true); + pool.bitmap[pool_section_offset..pool_section_end].fill(true); } } From e04cbaa54b90263eb0ae398f0f9e68c68aa304d5 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 7 Mar 2025 16:23:51 +0100 Subject: [PATCH 32/36] prepare benching of different segment sizes --- betree/Cargo.toml | 11 +++++++++++ betree/src/allocator/mod.rs | 38 ++++++++++++++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/betree/Cargo.toml b/betree/Cargo.toml index ada5cff8b..ec65d46ca 100644 --- a/betree/Cargo.toml +++ b/betree/Cargo.toml @@ -87,3 +87,14 @@ nvm = ["pmdk"] # Log the allocations and deallocations done for later analysis allocation_log = [] +# Used for benchmarking the allocators on different segment sizes +seg_log_2_14 = [] +seg_log_2_15 = [] +seg_log_2_16 = [] +seg_log_2_17 = [] +seg_log_2_18 = [] +seg_log_2_19 = [] +seg_log_2_20 = [] +seg_log_2_21 = [] +seg_log_2_22 = [] +seg_log_2_23 = [] diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 38ea7c9fc..3a1165a4d 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -49,7 +49,43 @@ pub use self::hybrid_allocator::HybridAllocator; pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap pub const SEGMENT_SIZE_BYTES: usize = SEGMENT_SIZE / 8; -const SEGMENT_SIZE_LOG_2: usize = 18; + +#[cfg(feature = "seg_log_2_14")] +pub const SEGMENT_SIZE_LOG_2: usize = 14; +#[cfg(feature = "seg_log_2_15")] +pub const SEGMENT_SIZE_LOG_2: usize = 15; +#[cfg(feature = "seg_log_2_16")] +pub const SEGMENT_SIZE_LOG_2: usize = 16; +#[cfg(feature = "seg_log_2_17")] +pub const SEGMENT_SIZE_LOG_2: usize = 17; +#[cfg(feature = "seg_log_2_18")] +pub const SEGMENT_SIZE_LOG_2: usize = 18; +#[cfg(feature = "seg_log_2_19")] +pub const SEGMENT_SIZE_LOG_2: usize = 19; +#[cfg(feature = "seg_log_2_20")] +pub const SEGMENT_SIZE_LOG_2: usize = 20; +#[cfg(feature = "seg_log_2_21")] +pub const SEGMENT_SIZE_LOG_2: usize = 21; +#[cfg(feature = "seg_log_2_22")] +pub const SEGMENT_SIZE_LOG_2: usize = 22; +#[cfg(feature = "seg_log_2_23")] +pub const SEGMENT_SIZE_LOG_2: usize = 23; +#[cfg(not(any( + feature = "seg_log_2_14", + feature = "seg_log_2_15", + feature = "seg_log_2_16", + feature = "seg_log_2_17", + feature = "seg_log_2_18", + feature = "seg_log_2_19", + feature = "seg_log_2_20", + feature = "seg_log_2_21", + feature = "seg_log_2_22", + feature = "seg_log_2_23" +)))] +/// Define SEGMENT_SIZE_LOG_2 based on feature flags used for benchmarking allocators based on +/// different segment sizes. +pub const SEGMENT_SIZE_LOG_2: usize = 18; + const SEGMENT_SIZE_MASK: usize = SEGMENT_SIZE - 1; /// The `AllocatorType` enum represents different strategies for allocating blocks From 8bd149cf5d97b9b61f11d18aa4409cbf1a5c2272 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Fri, 7 Mar 2025 16:25:09 +0100 Subject: [PATCH 33/36] scripts and small tweaks for benching allocators --- betree/benches/allocator.rs | 15 ++++--- betree/benches/benchmark_allocators | 15 +++++++ betree/benches/extract_results | 66 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 5 deletions(-) create mode 100755 betree/benches/benchmark_allocators create mode 100755 betree/benches/extract_results diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 618b8cb07..94825c576 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -181,10 +181,10 @@ pub fn criterion_benchmark(c: &mut Criterion) { let zipfian_exponent = 0.99; let distributions = [ - ( - "uniform", - SizeDistribution::Uniform(Uniform::new(min_size, max_size)), - ), + //( + // "uniform", + // SizeDistribution::Uniform(Uniform::new(min_size, max_size + 1)), + //), ( "zipfian", SizeDistribution::Zipfian( @@ -270,5 +270,10 @@ pub fn criterion_benchmark(c: &mut Criterion) { } } -criterion_group!(benches, criterion_benchmark); +criterion_group! { + name = benches; + // This can be any expression that returns a `Criterion` object. + config = Criterion::default().sample_size(500).measurement_time(Duration::new(120, 0)).warm_up_time(Duration::new(10, 0)).without_plots(); + targets = criterion_benchmark +} criterion_main!(benches); diff --git a/betree/benches/benchmark_allocators b/betree/benches/benchmark_allocators new file mode 100755 index 000000000..c594e2d14 --- /dev/null +++ b/betree/benches/benchmark_allocators @@ -0,0 +1,15 @@ +#!/bin/bash + +echo "Starting allocator benchmarks for different SEGMENT_SIZE_LOG_2 values..." + +for seg_log_2 in {14..23}; do + feature_name="seg_log_2_${seg_log_2}" + + echo "Running benchmark with SEGMENT_SIZE_LOG_2 = ${seg_log_2} (Feature: ${feature_name})" + + cargo bench --features "${feature_name}" > /dev/null 2>&1 -- allocator +done + +echo "All benchmarks completed. Results are in the 'target/criterion' directory." + + diff --git a/betree/benches/extract_results b/betree/benches/extract_results new file mode 100755 index 000000000..36f984214 --- /dev/null +++ b/betree/benches/extract_results @@ -0,0 +1,66 @@ +#!/bin/bash + +# Script to extract median benchmark times from Criterion JSON output and create CSV files. +# Usage: ./extract_results_csv.sh + +if [ -z "$1" ]; then + echo "Usage: ./extract_results " + echo "Please provide the path to the directory containing the criterion benchmark results." + exit 1 +fi + +RESULTS_DIR="$1" + +if [ ! -d "${RESULTS_DIR}" ]; then + echo "Error: Results directory '${RESULTS_DIR}' not found." + exit 1 +fi + +BENCHMARK_TYPES=("alloc" "new") +SEG_LOG_2_VALUES=({14..23}) +ALLOCATORS=( + "FirstFitList" + "FirstFitScan" + "FirstFitTree" + "NextFitList" + "NextFitScan" + "BestFitList" + "BestFitScan" + "BestFitTree" + "ApproximateBestFitTree" + "WorstFitList" + "WorstFitScan" + "WorstFitTree" + "SegmentAllocator" + "HybridAllocator" +) + +# Create CSV file headers +echo -e "SEGMENT_SIZE_LOG_2 ${ALLOCATORS[@]}" > alloc_benchmark_results.csv +echo -e "SEGMENT_SIZE_LOG_2 ${ALLOCATORS[@]}" > new_benchmark_results.csv + +for benchmark_type in "${BENCHMARK_TYPES[@]}"; do + csv_file="${benchmark_type}_benchmark_results.csv" + for seg_log_2 in "${SEG_LOG_2_VALUES[@]}"; do + line="${seg_log_2}" + for allocator in "${ALLOCATORS[@]}"; do + json_path="${RESULTS_DIR}/allocator_${benchmark_type}_zipfian_${seg_log_2}/${allocator}/new/estimates.json" + + if [ -f "${json_path}" ]; then + median_value=$(jq '.median.point_estimate' "${json_path}" 2>/dev/null) + if [ -n "$median_value" ]; then + line+=" ${median_value}" + else + line+=" N/A" + echo "Warning: jq failed or no median value found in ${json_path}" + fi + else + line+=" N/A" + echo "Warning: JSON file not found: ${json_path}" + fi + done + echo -e "${line}" >> "${csv_file}" + done +done + +echo "CSV files 'alloc_benchmark_results.csv' and 'new_benchmark_results.csv' created in the script directory." From ec2b929d660a1b225925abfa37f9eb8e9c229d04 Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Mon, 24 Mar 2025 14:29:12 +0100 Subject: [PATCH 34/36] preallocate vector in benchmark --- betree/benches/allocator.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 94825c576..d43b07e78 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -67,6 +67,7 @@ fn bench_alloc( ) { let data = [0; SEGMENT_SIZE_BYTES]; let mut allocated = Vec::new(); + allocated.reserve(allocations as usize); let mut rng = StdRng::seed_from_u64(42); let mut sample_size = || -> u32 { @@ -83,6 +84,7 @@ fn bench_alloc( let mut total_allocation_time = Duration::new(0, 0); for _ in 0..iters { + allocated.clear(); let mut allocator = A::new(data); let start = Instant::now(); @@ -126,6 +128,7 @@ fn bench_new( ) { let data = [0; SEGMENT_SIZE_BYTES]; let mut allocated = Vec::new(); + allocated.reserve(allocations as usize); let mut rng = StdRng::seed_from_u64(42); let mut sample_size = || -> u32 { @@ -142,6 +145,7 @@ fn bench_new( let mut total_allocation_time = Duration::new(0, 0); for _ in 0..iters { + allocated.clear(); let start = Instant::now(); let mut allocator = A::new(data); total_allocation_time += start.elapsed(); From 24c4033ded833bd55c3b7a0395fb720b26018f4f Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Thu, 8 May 2025 06:26:14 +0200 Subject: [PATCH 35/36] state of bachelorsthesis --- betree/Cargo.toml | 2 +- betree/benches/allocator.rs | 2 +- betree/benches/extract_results | 1 - betree/haura-benchmarks/run.sh | 2 +- betree/haura-benchmarks/src/ycsb.rs | 11 +- .../allocator/approximate_best_fit_tree.rs | 24 +- betree/src/allocator/first_fit_tree.rs | 24 +- betree/src/allocator/worst_fit_tree.rs | 24 +- betree/src/database/mod.rs | 2 +- scripts/alloc_cycles_to_csv | 230 ++++++++++++++++++ 10 files changed, 304 insertions(+), 18 deletions(-) create mode 100755 scripts/alloc_cycles_to_csv diff --git a/betree/Cargo.toml b/betree/Cargo.toml index ec65d46ca..76f2d7473 100644 --- a/betree/Cargo.toml +++ b/betree/Cargo.toml @@ -71,7 +71,7 @@ criterion = "0.3" zipf = "7.0.1" [features] -default = ["init_env_logger", "figment_config"] +default = ["init_env_logger", "figment_config", "allocation_log"] # unlock unstable API for consumption by bectl and other debugging tools internal-api = [] diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index d43b07e78..90bcf8725 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -277,7 +277,7 @@ pub fn criterion_benchmark(c: &mut Criterion) { criterion_group! { name = benches; // This can be any expression that returns a `Criterion` object. - config = Criterion::default().sample_size(500).measurement_time(Duration::new(120, 0)).warm_up_time(Duration::new(10, 0)).without_plots(); + config = Criterion::default().sample_size(500).measurement_time(Duration::new(600, 0)).warm_up_time(Duration::new(10, 0)); targets = criterion_benchmark } criterion_main!(benches); diff --git a/betree/benches/extract_results b/betree/benches/extract_results index 36f984214..3208e361c 100755 --- a/betree/benches/extract_results +++ b/betree/benches/extract_results @@ -32,7 +32,6 @@ ALLOCATORS=( "WorstFitScan" "WorstFitTree" "SegmentAllocator" - "HybridAllocator" ) # Create CSV file headers diff --git a/betree/haura-benchmarks/run.sh b/betree/haura-benchmarks/run.sh index ebe1cb16f..8592aa4eb 100755 --- a/betree/haura-benchmarks/run.sh +++ b/betree/haura-benchmarks/run.sh @@ -282,7 +282,7 @@ ensure_config #checkpoints #switchover #ingest -# ycsb_a +ycsb_a # ycsb_b # ycsb_c # ycsb_d diff --git a/betree/haura-benchmarks/src/ycsb.rs b/betree/haura-benchmarks/src/ycsb.rs index 1f31b9f35..35071f5e9 100644 --- a/betree/haura-benchmarks/src/ycsb.rs +++ b/betree/haura-benchmarks/src/ycsb.rs @@ -56,28 +56,31 @@ pub fn a(mut client: KvClient, size: u64, threads: usize, runtime: u64) { let mut w = std::io::BufWriter::new(f); w.write_all(b"threads,ops,time_ns\n").unwrap(); + let keys_arc = Arc::new(keys); + for workers in 1..=threads { println!("Running benchmark with {workers} threads..."); let threads = (0..workers) .map(|_| std::sync::mpsc::channel::()) .enumerate() .map(|(id, (tx, rx))| { - let keys = keys.clone(); + let keys_arc_clone = Arc::clone(&keys_arc); let ds = client.ds.clone(); ( std::thread::spawn(move || { let mut rng = rand_xoshiro::Xoshiro256Plus::seed_from_u64(id as u64); - let dist = zipf::ZipfDistribution::new(keys.len(), ZIPF_EXP).unwrap(); + let dist = + zipf::ZipfDistribution::new(keys_arc_clone.len(), ZIPF_EXP).unwrap(); let mut total = 0; let value = vec![0u8; ENTRY_SIZE]; while let Ok(start) = rx.recv() { while start.elapsed().as_secs() < runtime { for _ in 0..100 { - let k = &keys[dist.sample(&mut rng) - 1][..]; + let k = &keys_arc_clone[dist.sample(&mut rng) - 1][..]; if rng.gen_bool(0.5) { ds.get(k).unwrap().unwrap(); } else { - ds.upsert(k.to_vec(), &value, 0).unwrap(); + ds.upsert(k, &value, 0).unwrap(); } total += 1; } diff --git a/betree/src/allocator/approximate_best_fit_tree.rs b/betree/src/allocator/approximate_best_fit_tree.rs index 055c7262c..8b156ce9a 100644 --- a/betree/src/allocator/approximate_best_fit_tree.rs +++ b/betree/src/allocator/approximate_best_fit_tree.rs @@ -95,9 +95,27 @@ impl Allocator for ApproximateBestFitTree { } fn allocate_at(&mut self, size: u32, offset: u32) -> bool { - // Because the tree is sorted by offset because of how it's build, this shouldn't be to - // hard to implement efficiently - todo!() + // NOTE: Because the tree is sorted by offset because of how it's build, this shouldn't be + // to hard to implement efficiently if needed. + + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + + // Rebuild the tree to reflect changes. This is **not** efficient but the easiest solution, + // as the allocate_at is called only **once** on loading the bitmap from disk. + self.build_fsm_tree(); + true } } diff --git a/betree/src/allocator/first_fit_tree.rs b/betree/src/allocator/first_fit_tree.rs index 751070279..862f11bca 100644 --- a/betree/src/allocator/first_fit_tree.rs +++ b/betree/src/allocator/first_fit_tree.rs @@ -88,9 +88,27 @@ impl Allocator for FirstFitTree { } fn allocate_at(&mut self, size: u32, offset: u32) -> bool { - // Because the tree is sorted by offset because of how it's build, this shouldn't be to - // hard to implement efficiently - todo!() + // NOTE: Because the tree is sorted by offset because of how it's build, this shouldn't be + // to hard to implement efficiently if needed. + + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + + // Rebuild the tree to reflect changes. This is **not** efficient but the easiest solution, + // as the allocate_at is called only **once** on loading the bitmap from disk. + self.build_fsm_tree(); + true } } diff --git a/betree/src/allocator/worst_fit_tree.rs b/betree/src/allocator/worst_fit_tree.rs index 5d41e0969..6b05bd4fc 100644 --- a/betree/src/allocator/worst_fit_tree.rs +++ b/betree/src/allocator/worst_fit_tree.rs @@ -93,9 +93,27 @@ impl Allocator for WorstFitTree { } fn allocate_at(&mut self, size: u32, offset: u32) -> bool { - // Because the tree is sorted by offset because of how it's build, this shouldn't be to - // hard to implement efficiently - todo!() + // NOTE: Because the tree is sorted by offset because of how it's build, this shouldn't be + // to hard to implement efficiently if needed. + + if size == 0 { + return true; + } + if offset + size > SEGMENT_SIZE as u32 { + return false; + } + + let start_idx = offset as usize; + let end_idx = (offset + size) as usize; + if self.data[start_idx..end_idx].any() { + return false; + } + self.mark(offset, size, Action::Allocate); + + // Rebuild the tree to reflect changes. This is **not** efficient but the easiest solution, + // as the allocate_at is called only **once** on loading the bitmap from disk. + self.build_fsm_tree(); + true } } diff --git a/betree/src/database/mod.rs b/betree/src/database/mod.rs index 070805167..a131789eb 100644 --- a/betree/src/database/mod.rs +++ b/betree/src/database/mod.rs @@ -170,7 +170,7 @@ impl Default for DatabaseConfiguration { metrics: None, migration_policy: None, allocation_log_file_path: PathBuf::from("allocation_log.bin"), - allocator: AllocatorType::SegmentAllocator, + allocator: AllocatorType::NextFitScan, } } } diff --git a/scripts/alloc_cycles_to_csv b/scripts/alloc_cycles_to_csv new file mode 100755 index 000000000..32b05a446 --- /dev/null +++ b/scripts/alloc_cycles_to_csv @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 + +import argparse +import csv +import struct +import os +from typing import Iterator, Any, IO + +# --- Constants and Classes Copied/Adapted from the Original Script --- + +# Constants to get relevant information from the disk_offset. +MASK_LAYER_ID = ((1 << 2) - 1) << (10 + 52) +MASK_DISK_ID = ((1 << 10) - 1) << 52 +MASK_OFFSET = (1 << 52) - 1 +SEGMENT_SIZE_LOG_2 = 18 +SEGMENT_SIZE = 1 << SEGMENT_SIZE_LOG_2 +SEGMENT_SIZE_MASK = SEGMENT_SIZE - 1 +# This is the amount of bytes one (de-)allocation has in the log. +SIZE_PER_ALLOCATION = 29 + + +class StorageConfig: + """Represents the storage configuration of the system (needed for header parsing).""" + + def __init__(self, num_layers: int, disks_per_layer: list[int], + blocks_per_disk: list[list[int]], blocks_per_segment: int): + self.num_layers = num_layers + self.disks_per_layer = disks_per_layer + self.blocks_per_disk = blocks_per_disk + self.blocks_per_segment = blocks_per_segment + # We don't need the other methods for this script + + +class Timestamp: + time: int + op_type: int + offset: int + num_blocks: int + cycles_alloc: int + cycles_total: int + layer_id: int + disk_id: int + block_offset: int + segment_id: int + segment_offset: int + + def __init__(self, op_type: int, offset: int, num_blocks: int, cycles_alloc: int, cycles_total: int, time: int): + self.op_type = op_type + self.offset = offset + self.num_blocks = num_blocks + self.cycles_alloc = cycles_alloc + self.cycles_total = cycles_total + self.time = time + self._parse_offset() + + def __str__(self) -> str: + return (f"Timestep(op_type: {self.op_type}, " + f"offset: {self.offset}, " + f"num_blocks: {self.num_blocks}, " + f"cycles_alloc: {self.cycles_alloc}, " + f"cycles_total: {self.cycles_total}, " + f"time: {self.time}, " + f"layer_id: {self.layer_id}, " + f"disk_id: {self.disk_id}, " + f"block_offset: {self.block_offset}, " + f"segment_id: {self.segment_id}, " + f"segment_offset: {self.segment_offset})") + + def _parse_offset(self): + """Parses the offset into human readable values""" + self.layer_id = (self.offset & MASK_LAYER_ID) >> (52 + 10) + self.disk_id = (self.offset & MASK_DISK_ID) >> 52 + self.block_offset = self.offset & MASK_OFFSET + # In haura the segment id is a multiple of the segment size. This is ugly for plotting. + self.segment_id = (self.block_offset & ~SEGMENT_SIZE_MASK) // SEGMENT_SIZE + self.segment_offset = self.block_offset % SEGMENT_SIZE + + +class Parser: + """Parses the allocation log file.""" + log_file: str + _file_handle: IO[Any] + timesteps: int + time: int + + def __init__(self, log_file: str): + self.log_file = log_file + self._file_handle = open(log_file, "rb") # Open the file in binary mode + + # Precalculate the number of timesteps. + _ = self.parse_header() + self.timesteps = self._remaining_bytes() // SIZE_PER_ALLOCATION + self._file_handle.seek(0) + + def __del__(self): + try: + self._file_handle.close() + except AttributeError: + # Happens when the file does not exist + pass + + def __len__(self) -> int: + return self.timesteps + + def parse_header(self) -> StorageConfig: + """Parses the header of the log file and returns a StorageConfig.""" + f = self._file_handle + num_classes = struct.unpack(" Iterator[Timestamp]: + """Prepares the iterator by skipping the header. Returns itself as the iterator.""" + self._file_handle.seek(0) + _ = self.parse_header() + self.time = 0 + return self + + def __next__(self) -> Timestamp: + """Reads the next allocation from the log file and returns a timestamp.""" + try: + op_type = struct.unpack(" int: + """Returns the remaining bytes in a file from the current position of the file pointer.""" + f = self._file_handle + current_position = f.tell() + f.seek(0, os.SEEK_END) + end_position = f.tell() + # Return to the original position. + f.seek(current_position, os.SEEK_SET) + return end_position - current_position + + +def main(): + """ + Parses the allocation log file, filters for allocation events up to an + optional maximum count, and writes the allocation count number and + local allocation cycles to a specified output CSV file. + """ + parser = argparse.ArgumentParser( + description="Parse allocation log and output allocation count and cycles to a CSV file, with an optional limit." + ) + parser.add_argument("input_log_file", help="Path to the input binary log file.") + parser.add_argument("output_csv_file", help="Path to the output CSV file.") + parser.add_argument( + "--max-allocations", + "-n", + type=int, + default=None, # Default is None, meaning no limit unless specified + help="Maximum number of allocation entries to write to the output file. If not set, all allocations are written." + ) + args = parser.parse_args() + + max_alloc_limit = args.max_allocations + + # Optional: Add a check for invalid limit values + if max_alloc_limit is not None and max_alloc_limit <= 0: + print("Error: --max-allocations must be a positive integer if specified.") + exit(1) + + try: + log_parser = Parser(args.input_log_file) + except FileNotFoundError: + print(f"Error: Input file not found at {args.input_log_file}") + exit(1) # Exit if input file is not found + except Exception as e: + print(f"Error initializing parser: {e}") + exit(1) # Exit on other parser init errors + + allocation_count = 0 # Initialize allocation counter + logged_allocations = 0 # Counter for allocations actually logged + + try: + with open(args.output_csv_file, 'w', newline='') as csvfile: + csv_writer = csv.writer(csvfile) + # Write the header row + csv_writer.writerow(['allocation_count', 'allocation_cycles_local']) + + # Iterate through log entries generated by the parser + for timestamp_entry in log_parser: + # Filter for entries that represent an allocation. + # NOTE: Assuming op_type == 1 signifies an allocation. + # Please adjust this value if your log uses a different indicator. + if timestamp_entry.op_type == 1: + allocation_count += 1 # Increment total allocation counter + + # Check if the limit has been reached *before* writing + if max_alloc_limit is not None and logged_allocations >= max_alloc_limit: + print(f"Reached specified maximum allocation limit ({max_alloc_limit}). Stopping log output.") + break # Exit the loop, no more entries will be processed or written + + # Write the relevant data: allocation count and cycles_alloc + csv_writer.writerow([allocation_count, timestamp_entry.cycles_alloc]) + logged_allocations += 1 # Increment counter for logged allocations + + print(f"Processed {allocation_count} total allocations.") + print(f"Successfully wrote {logged_allocations} allocation entries to {args.output_csv_file}") + + except IOError as e: + print(f"Error writing to output file {args.output_csv_file}: {e}") + exit(1) # Exit on file writing error + except Exception as e: + print(f"An unexpected error occurred during processing: {e}") + exit(1) # Exit on other processing errors + + +if __name__ == "__main__": + main() From b7b64a7a7c016b567a7a7c4f44dc2277f0f5d13a Mon Sep 17 00:00:00 2001 From: Pascal Zittlau Date: Wed, 20 Aug 2025 22:00:45 +0200 Subject: [PATCH 36/36] pr state --- betree/Cargo.toml | 13 +--- betree/benches/allocator.rs | 26 ++++---- betree/benches/benchmark_allocators | 15 ----- betree/benches/extract_results | 65 ------------------- betree/haura-benchmarks/run.sh | 2 +- betree/haura-benchmarks/src/ycsb.rs | 11 ++-- ...imate_best_fit_tree.rs => best_fit_fsm.rs} | 26 ++++---- .../{first_fit_tree.rs => first_fit_fsm.rs} | 26 ++++---- betree/src/allocator/mod.rs | 44 ++----------- .../{worst_fit_tree.rs => worst_fit_fsm.rs} | 26 ++++---- betree/src/c_interface.rs | 26 ++------ betree/src/database/handler.rs | 6 +- 12 files changed, 71 insertions(+), 215 deletions(-) delete mode 100755 betree/benches/benchmark_allocators delete mode 100755 betree/benches/extract_results rename betree/src/allocator/{approximate_best_fit_tree.rs => best_fit_fsm.rs} (93%) rename betree/src/allocator/{first_fit_tree.rs => first_fit_fsm.rs} (93%) rename betree/src/allocator/{worst_fit_tree.rs => worst_fit_fsm.rs} (93%) diff --git a/betree/Cargo.toml b/betree/Cargo.toml index 76f2d7473..ada5cff8b 100644 --- a/betree/Cargo.toml +++ b/betree/Cargo.toml @@ -71,7 +71,7 @@ criterion = "0.3" zipf = "7.0.1" [features] -default = ["init_env_logger", "figment_config", "allocation_log"] +default = ["init_env_logger", "figment_config"] # unlock unstable API for consumption by bectl and other debugging tools internal-api = [] @@ -87,14 +87,3 @@ nvm = ["pmdk"] # Log the allocations and deallocations done for later analysis allocation_log = [] -# Used for benchmarking the allocators on different segment sizes -seg_log_2_14 = [] -seg_log_2_15 = [] -seg_log_2_16 = [] -seg_log_2_17 = [] -seg_log_2_18 = [] -seg_log_2_19 = [] -seg_log_2_20 = [] -seg_log_2_21 = [] -seg_log_2_22 = [] -seg_log_2_23 = [] diff --git a/betree/benches/allocator.rs b/betree/benches/allocator.rs index 90bcf8725..47dab75fe 100644 --- a/betree/benches/allocator.rs +++ b/betree/benches/allocator.rs @@ -1,9 +1,9 @@ use std::time::{Duration, Instant}; use betree_storage_stack::allocator::{ - self, Allocator, ApproximateBestFitTree, BestFitList, BestFitScan, BestFitTree, FirstFitList, - FirstFitScan, FirstFitTree, HybridAllocator, NextFitList, NextFitScan, SegmentAllocator, - WorstFitList, WorstFitScan, WorstFitTree, SEGMENT_SIZE_BYTES, SEGMENT_SIZE_LOG_2, + self, Allocator, BestFitFSM, BestFitList, BestFitScan, BestFitTree, FirstFitFSM, FirstFitList, + FirstFitScan, HybridAllocator, NextFitList, NextFitScan, SegmentAllocator, WorstFitFSM, + WorstFitList, WorstFitScan, SEGMENT_SIZE_BYTES, SEGMENT_SIZE_LOG_2, }; use criterion::{black_box, criterion_group, criterion_main, Bencher, Criterion}; use rand::{ @@ -185,10 +185,10 @@ pub fn criterion_benchmark(c: &mut Criterion) { let zipfian_exponent = 0.99; let distributions = [ - //( - // "uniform", - // SizeDistribution::Uniform(Uniform::new(min_size, max_size + 1)), - //), + ( + "uniform", + SizeDistribution::Uniform(Uniform::new(min_size, max_size + 1)), + ), ( "zipfian", SizeDistribution::Zipfian( @@ -206,16 +206,16 @@ pub fn criterion_benchmark(c: &mut Criterion) { bench_alloc, FirstFitScan, FirstFitList, - FirstFitTree, + FirstFitFSM, NextFitScan, NextFitList, BestFitScan, BestFitList, - ApproximateBestFitTree, + BestFitFSM, BestFitTree, WorstFitScan, WorstFitList, - WorstFitTree, + WorstFitFSM, SegmentAllocator ); @@ -242,16 +242,16 @@ pub fn criterion_benchmark(c: &mut Criterion) { bench_new, FirstFitScan, FirstFitList, - FirstFitTree, + FirstFitFSM, NextFitScan, NextFitList, BestFitScan, BestFitList, - ApproximateBestFitTree, + BestFitFSM, BestFitTree, WorstFitScan, WorstFitList, - WorstFitTree, + WorstFitFSM, SegmentAllocator ); diff --git a/betree/benches/benchmark_allocators b/betree/benches/benchmark_allocators deleted file mode 100755 index c594e2d14..000000000 --- a/betree/benches/benchmark_allocators +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -echo "Starting allocator benchmarks for different SEGMENT_SIZE_LOG_2 values..." - -for seg_log_2 in {14..23}; do - feature_name="seg_log_2_${seg_log_2}" - - echo "Running benchmark with SEGMENT_SIZE_LOG_2 = ${seg_log_2} (Feature: ${feature_name})" - - cargo bench --features "${feature_name}" > /dev/null 2>&1 -- allocator -done - -echo "All benchmarks completed. Results are in the 'target/criterion' directory." - - diff --git a/betree/benches/extract_results b/betree/benches/extract_results deleted file mode 100755 index 3208e361c..000000000 --- a/betree/benches/extract_results +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# Script to extract median benchmark times from Criterion JSON output and create CSV files. -# Usage: ./extract_results_csv.sh - -if [ -z "$1" ]; then - echo "Usage: ./extract_results " - echo "Please provide the path to the directory containing the criterion benchmark results." - exit 1 -fi - -RESULTS_DIR="$1" - -if [ ! -d "${RESULTS_DIR}" ]; then - echo "Error: Results directory '${RESULTS_DIR}' not found." - exit 1 -fi - -BENCHMARK_TYPES=("alloc" "new") -SEG_LOG_2_VALUES=({14..23}) -ALLOCATORS=( - "FirstFitList" - "FirstFitScan" - "FirstFitTree" - "NextFitList" - "NextFitScan" - "BestFitList" - "BestFitScan" - "BestFitTree" - "ApproximateBestFitTree" - "WorstFitList" - "WorstFitScan" - "WorstFitTree" - "SegmentAllocator" -) - -# Create CSV file headers -echo -e "SEGMENT_SIZE_LOG_2 ${ALLOCATORS[@]}" > alloc_benchmark_results.csv -echo -e "SEGMENT_SIZE_LOG_2 ${ALLOCATORS[@]}" > new_benchmark_results.csv - -for benchmark_type in "${BENCHMARK_TYPES[@]}"; do - csv_file="${benchmark_type}_benchmark_results.csv" - for seg_log_2 in "${SEG_LOG_2_VALUES[@]}"; do - line="${seg_log_2}" - for allocator in "${ALLOCATORS[@]}"; do - json_path="${RESULTS_DIR}/allocator_${benchmark_type}_zipfian_${seg_log_2}/${allocator}/new/estimates.json" - - if [ -f "${json_path}" ]; then - median_value=$(jq '.median.point_estimate' "${json_path}" 2>/dev/null) - if [ -n "$median_value" ]; then - line+=" ${median_value}" - else - line+=" N/A" - echo "Warning: jq failed or no median value found in ${json_path}" - fi - else - line+=" N/A" - echo "Warning: JSON file not found: ${json_path}" - fi - done - echo -e "${line}" >> "${csv_file}" - done -done - -echo "CSV files 'alloc_benchmark_results.csv' and 'new_benchmark_results.csv' created in the script directory." diff --git a/betree/haura-benchmarks/run.sh b/betree/haura-benchmarks/run.sh index 8592aa4eb..ebe1cb16f 100755 --- a/betree/haura-benchmarks/run.sh +++ b/betree/haura-benchmarks/run.sh @@ -282,7 +282,7 @@ ensure_config #checkpoints #switchover #ingest -ycsb_a +# ycsb_a # ycsb_b # ycsb_c # ycsb_d diff --git a/betree/haura-benchmarks/src/ycsb.rs b/betree/haura-benchmarks/src/ycsb.rs index 35071f5e9..1f31b9f35 100644 --- a/betree/haura-benchmarks/src/ycsb.rs +++ b/betree/haura-benchmarks/src/ycsb.rs @@ -56,31 +56,28 @@ pub fn a(mut client: KvClient, size: u64, threads: usize, runtime: u64) { let mut w = std::io::BufWriter::new(f); w.write_all(b"threads,ops,time_ns\n").unwrap(); - let keys_arc = Arc::new(keys); - for workers in 1..=threads { println!("Running benchmark with {workers} threads..."); let threads = (0..workers) .map(|_| std::sync::mpsc::channel::()) .enumerate() .map(|(id, (tx, rx))| { - let keys_arc_clone = Arc::clone(&keys_arc); + let keys = keys.clone(); let ds = client.ds.clone(); ( std::thread::spawn(move || { let mut rng = rand_xoshiro::Xoshiro256Plus::seed_from_u64(id as u64); - let dist = - zipf::ZipfDistribution::new(keys_arc_clone.len(), ZIPF_EXP).unwrap(); + let dist = zipf::ZipfDistribution::new(keys.len(), ZIPF_EXP).unwrap(); let mut total = 0; let value = vec![0u8; ENTRY_SIZE]; while let Ok(start) = rx.recv() { while start.elapsed().as_secs() < runtime { for _ in 0..100 { - let k = &keys_arc_clone[dist.sample(&mut rng) - 1][..]; + let k = &keys[dist.sample(&mut rng) - 1][..]; if rng.gen_bool(0.5) { ds.get(k).unwrap().unwrap(); } else { - ds.upsert(k, &value, 0).unwrap(); + ds.upsert(k.to_vec(), &value, 0).unwrap(); } total += 1; } diff --git a/betree/src/allocator/approximate_best_fit_tree.rs b/betree/src/allocator/best_fit_fsm.rs similarity index 93% rename from betree/src/allocator/approximate_best_fit_tree.rs rename to betree/src/allocator/best_fit_fsm.rs index 8b156ce9a..012026b2d 100644 --- a/betree/src/allocator/approximate_best_fit_tree.rs +++ b/betree/src/allocator/best_fit_fsm.rs @@ -4,13 +4,13 @@ use super::*; /// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README /// This is an approximate best fit allocator. It will not always find the best fit but it tries /// its best. -pub struct ApproximateBestFitTree { +pub struct BestFitFSM { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space tree_height: u32, } -impl Allocator for ApproximateBestFitTree { +impl Allocator for BestFitFSM { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -19,7 +19,7 @@ impl Allocator for ApproximateBestFitTree { /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); - let mut allocator = ApproximateBestFitTree { + let mut allocator = BestFitFSM { data, fsm_tree: Vec::new(), tree_height: 0, @@ -119,7 +119,7 @@ impl Allocator for ApproximateBestFitTree { } } -impl ApproximateBestFitTree { +impl BestFitFSM { fn get_free_segments(&mut self) -> Vec<(u32, u32)> { let mut offset: u32 = 0; let mut free_segments = Vec::new(); @@ -193,7 +193,7 @@ mod tests { #[test] fn build_empty() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let allocator = ApproximateBestFitTree::new(bitmap); + let allocator = BestFitFSM::new(bitmap); // In an empty bitmap, the root node should have a large free space assert_eq!(allocator.fsm_tree[0].0, 0 as u32); @@ -204,14 +204,14 @@ mod tests { #[test] fn build_simple() { // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free - let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones - let mut allocator = ApproximateBestFitTree::new(bitmap.into_inner()); + let mut allocator = BestFitFSM::new(bitmap.into_inner()); let fsm_tree = vec![ (7, SEGMENT_SIZE as u32 - 7), @@ -224,7 +224,7 @@ mod tests { #[test] fn build_complex() { - let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -235,7 +235,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let allocator = ApproximateBestFitTree::new(bitmap.into_inner()); + let allocator = BestFitFSM::new(bitmap.into_inner()); // binary heap layout let fsm_tree = vec![ @@ -263,7 +263,7 @@ mod tests { #[test] fn allocate_empty_fsm_tree() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let mut allocator = ApproximateBestFitTree::new(bitmap); + let mut allocator = BestFitFSM::new(bitmap); let allocation = allocator.allocate(1024); assert!(allocation.is_some()); // Allocation should succeed @@ -279,7 +279,7 @@ mod tests { #[test] fn allocate_complex_fsm_tree() { - let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -290,7 +290,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let mut allocator = ApproximateBestFitTree::new(bitmap.into_inner()); + let mut allocator = BestFitFSM::new(bitmap.into_inner()); // Best-fit should allocate from the segment at offset 3 with size 2 let allocation = allocator.allocate(2); // Request allocation of size 2 @@ -314,7 +314,7 @@ mod tests { #[test] fn allocate_fail_fsm_tree() { - let mut allocator = ApproximateBestFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = BestFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let root_free_space = allocator.fsm_tree[0].1; // Try to allocate more than available space diff --git a/betree/src/allocator/first_fit_tree.rs b/betree/src/allocator/first_fit_fsm.rs similarity index 93% rename from betree/src/allocator/first_fit_tree.rs rename to betree/src/allocator/first_fit_fsm.rs index 862f11bca..6dab9c214 100644 --- a/betree/src/allocator/first_fit_tree.rs +++ b/betree/src/allocator/first_fit_fsm.rs @@ -2,13 +2,13 @@ use super::*; /// Based on the free-space-map allocator from postgresql: /// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README -pub struct FirstFitTree { +pub struct FirstFitFSM { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space tree_height: u32, } -impl Allocator for FirstFitTree { +impl Allocator for FirstFitFSM { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -17,7 +17,7 @@ impl Allocator for FirstFitTree { /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); - let mut allocator = FirstFitTree { + let mut allocator = FirstFitFSM { data, fsm_tree: Vec::new(), tree_height: 0, @@ -112,7 +112,7 @@ impl Allocator for FirstFitTree { } } -impl FirstFitTree { +impl FirstFitFSM { fn get_free_segments(&mut self) -> Vec<(u32, u32)> { let mut offset: u32 = 0; let mut free_segments = Vec::new(); @@ -186,7 +186,7 @@ mod tests { #[test] fn build_empty() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let allocator = FirstFitTree::new(bitmap); + let allocator = FirstFitFSM::new(bitmap); // In an empty bitmap, the root node should have a large free space assert_eq!(allocator.fsm_tree[0].0, 0 as u32); @@ -197,14 +197,14 @@ mod tests { #[test] fn build_simple() { // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free - let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones - let mut allocator = FirstFitTree::new(bitmap.into_inner()); + let mut allocator = FirstFitFSM::new(bitmap.into_inner()); let fsm_tree = vec![ (7, SEGMENT_SIZE as u32 - 7), @@ -217,7 +217,7 @@ mod tests { #[test] fn build_complex() { - let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -228,7 +228,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let allocator = FirstFitTree::new(bitmap.into_inner()); + let allocator = FirstFitFSM::new(bitmap.into_inner()); // binary heap layout let fsm_tree = vec![ @@ -256,7 +256,7 @@ mod tests { #[test] fn allocate_empty_fsm_tree() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let mut allocator = FirstFitTree::new(bitmap); + let mut allocator = FirstFitFSM::new(bitmap); let allocation = allocator.allocate(1024); assert!(allocation.is_some()); // Allocation should succeed @@ -272,7 +272,7 @@ mod tests { #[test] fn allocate_complex_fsm_tree() { - let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -283,7 +283,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let mut allocator = FirstFitTree::new(bitmap.into_inner()); + let mut allocator = FirstFitFSM::new(bitmap.into_inner()); // Best-fit should allocate from the segment at offset 3 with size 2 let allocation = allocator.allocate(2); // Request allocation of size 2 @@ -307,7 +307,7 @@ mod tests { #[test] fn allocate_fail_fsm_tree() { - let mut allocator = FirstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = FirstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let root_free_space = allocator.fsm_tree[0].1; // Try to allocate more than available space diff --git a/betree/src/allocator/mod.rs b/betree/src/allocator/mod.rs index 3a1165a4d..c9b1a68cc 100644 --- a/betree/src/allocator/mod.rs +++ b/betree/src/allocator/mod.rs @@ -30,17 +30,17 @@ pub use self::best_fit_list::BestFitList; mod worst_fit_list; pub use self::worst_fit_list::WorstFitList; -mod first_fit_tree; -pub use self::first_fit_tree::FirstFitTree; +mod first_fit_fsm; +pub use self::first_fit_fsm::FirstFitFSM; mod best_fit_tree; pub use self::best_fit_tree::BestFitTree; -mod approximate_best_fit_tree; -pub use self::approximate_best_fit_tree::ApproximateBestFitTree; +mod best_fit_fsm; +pub use self::best_fit_fsm::BestFitFSM; -mod worst_fit_tree; -pub use self::worst_fit_tree::WorstFitTree; +mod worst_fit_fsm; +pub use self::worst_fit_fsm::WorstFitFSM; mod hybrid_allocator; pub use self::hybrid_allocator::HybridAllocator; @@ -50,38 +50,6 @@ pub const SEGMENT_SIZE: usize = 1 << SEGMENT_SIZE_LOG_2; /// Number of bytes required to store a segments allocation bitmap pub const SEGMENT_SIZE_BYTES: usize = SEGMENT_SIZE / 8; -#[cfg(feature = "seg_log_2_14")] -pub const SEGMENT_SIZE_LOG_2: usize = 14; -#[cfg(feature = "seg_log_2_15")] -pub const SEGMENT_SIZE_LOG_2: usize = 15; -#[cfg(feature = "seg_log_2_16")] -pub const SEGMENT_SIZE_LOG_2: usize = 16; -#[cfg(feature = "seg_log_2_17")] -pub const SEGMENT_SIZE_LOG_2: usize = 17; -#[cfg(feature = "seg_log_2_18")] -pub const SEGMENT_SIZE_LOG_2: usize = 18; -#[cfg(feature = "seg_log_2_19")] -pub const SEGMENT_SIZE_LOG_2: usize = 19; -#[cfg(feature = "seg_log_2_20")] -pub const SEGMENT_SIZE_LOG_2: usize = 20; -#[cfg(feature = "seg_log_2_21")] -pub const SEGMENT_SIZE_LOG_2: usize = 21; -#[cfg(feature = "seg_log_2_22")] -pub const SEGMENT_SIZE_LOG_2: usize = 22; -#[cfg(feature = "seg_log_2_23")] -pub const SEGMENT_SIZE_LOG_2: usize = 23; -#[cfg(not(any( - feature = "seg_log_2_14", - feature = "seg_log_2_15", - feature = "seg_log_2_16", - feature = "seg_log_2_17", - feature = "seg_log_2_18", - feature = "seg_log_2_19", - feature = "seg_log_2_20", - feature = "seg_log_2_21", - feature = "seg_log_2_22", - feature = "seg_log_2_23" -)))] /// Define SEGMENT_SIZE_LOG_2 based on feature flags used for benchmarking allocators based on /// different segment sizes. pub const SEGMENT_SIZE_LOG_2: usize = 18; diff --git a/betree/src/allocator/worst_fit_tree.rs b/betree/src/allocator/worst_fit_fsm.rs similarity index 93% rename from betree/src/allocator/worst_fit_tree.rs rename to betree/src/allocator/worst_fit_fsm.rs index 6b05bd4fc..3fc3c7104 100644 --- a/betree/src/allocator/worst_fit_tree.rs +++ b/betree/src/allocator/worst_fit_fsm.rs @@ -2,13 +2,13 @@ use super::*; /// Based on the free-space-map allocator from postgresql: /// https://github.com/postgres/postgres/blob/02ed3c2bdcefab453b548bc9c7e0e8874a502790/src/backend/storage/freespace/README -pub struct WorstFitTree { +pub struct WorstFitFSM { data: BitArr!(for SEGMENT_SIZE, in u8, Lsb0), fsm_tree: Vec<(u32, u32)>, // Array to represent the FSM tree, storing max free space tree_height: u32, } -impl Allocator for WorstFitTree { +impl Allocator for WorstFitFSM { fn data(&mut self) -> &mut BitArr!(for SEGMENT_SIZE, in u8, Lsb0) { &mut self.data } @@ -17,7 +17,7 @@ impl Allocator for WorstFitTree { /// The `bitmap` must have a length of `SEGMENT_SIZE`. fn new(bitmap: [u8; SEGMENT_SIZE_BYTES]) -> Self { let data = BitArray::new(bitmap); - let mut allocator = WorstFitTree { + let mut allocator = WorstFitFSM { data, fsm_tree: Vec::new(), tree_height: 0, @@ -117,7 +117,7 @@ impl Allocator for WorstFitTree { } } -impl WorstFitTree { +impl WorstFitFSM { fn get_free_segments(&mut self) -> Vec<(u32, u32)> { let mut offset: u32 = 0; let mut free_segments = Vec::new(); @@ -191,7 +191,7 @@ mod tests { #[test] fn build_empty() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let allocator = WorstFitTree::new(bitmap); + let allocator = WorstFitFSM::new(bitmap); // In an empty bitmap, the root node should have a large free space assert_eq!(allocator.fsm_tree[0].0, 0 as u32); @@ -202,14 +202,14 @@ mod tests { #[test] fn build_simple() { // Example bitmap: 3 segments allocated at the beginning, 2 free, 3 allocated, rest free - let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments bitmap[0..3].fill(true); // Allocate 3 blocks at the beginning bitmap[5..7].fill(true); // Allocate 2 blocks after the free ones - let mut allocator = WorstFitTree::new(bitmap.into_inner()); + let mut allocator = WorstFitFSM::new(bitmap.into_inner()); let fsm_tree = vec![ (7, SEGMENT_SIZE as u32 - 7), @@ -222,7 +222,7 @@ mod tests { #[test] fn build_complex() { - let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -233,7 +233,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let allocator = WorstFitTree::new(bitmap.into_inner()); + let allocator = WorstFitFSM::new(bitmap.into_inner()); // binary heap layout let fsm_tree = vec![ @@ -261,7 +261,7 @@ mod tests { #[test] fn allocate_empty_fsm_tree() { let bitmap = [0u8; SEGMENT_SIZE_BYTES]; - let mut allocator = WorstFitTree::new(bitmap); + let mut allocator = WorstFitFSM::new(bitmap); let allocation = allocator.allocate(1024); assert!(allocation.is_some()); // Allocation should succeed @@ -277,7 +277,7 @@ mod tests { #[test] fn allocate_complex_fsm_tree() { - let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let bitmap = allocator.data(); // Manually allocate some segments to create a non-trivial tree @@ -288,7 +288,7 @@ mod tests { bitmap[35..36].fill(true); bitmap[42..53].fill(true); - let mut allocator = WorstFitTree::new(bitmap.into_inner()); + let mut allocator = WorstFitFSM::new(bitmap.into_inner()); // Worst-fit should allocate from the segment at offset 3 with size 2 let allocation = allocator.allocate(2); // Request allocation of size 2 @@ -312,7 +312,7 @@ mod tests { #[test] fn allocate_fail_fsm_tree() { - let mut allocator = WorstFitTree::new([0u8; SEGMENT_SIZE_BYTES]); + let mut allocator = WorstFitFSM::new([0u8; SEGMENT_SIZE_BYTES]); let root_free_space = allocator.fsm_tree[0].1; // Try to allocate more than available space diff --git a/betree/src/c_interface.rs b/betree/src/c_interface.rs index f4b432d29..b4717b08d 100644 --- a/betree/src/c_interface.rs +++ b/betree/src/c_interface.rs @@ -4,7 +4,6 @@ use std::{ env::SplitPaths, ffi::{CStr, OsStr}, io::{stderr, BufReader, Write}, - ops::Deref, os::{ raw::{c_char, c_int, c_uint, c_ulong}, unix::prelude::OsStrExt, @@ -16,11 +15,9 @@ use std::{ }; use libc::{c_void, memcpy}; -use parking_lot::RwLock; use crate::{ cow_bytes::{CowBytes, SlicedCowBytes}, - data_management::Dml, database::{AccessMode, Database, Dataset, Error, Snapshot}, object::{ObjectHandle, ObjectStore}, storage_pool::{LeafVdev, StoragePoolConfiguration, TierConfiguration, Vdev}, @@ -127,20 +124,6 @@ impl HandleResult for Database { } } -impl HandleResult for Arc> { - type Result = *mut db_t; - fn success(self) -> *mut db_t { - unsafe { - let rwlock_db = Arc::into_raw(self); - let db = rwlock_db.read(); - b(db_t(db.into_inner())) - } - } - fn fail() -> *mut db_t { - null_mut() - } -} - impl HandleResult for Dataset { type Result = *mut ds_t; fn success(self) -> *mut ds_t { @@ -395,7 +378,7 @@ pub unsafe extern "C" fn betree_configuration_set_disks( /// On error, return null. If `err` is not null, store an error in `err`. #[no_mangle] pub unsafe extern "C" fn betree_build_db(cfg: *const cfg_t, err: *mut *mut err_t) -> *mut db_t { - Database::build_threaded((*cfg).0.clone()).handle_result(err) + Database::build((*cfg).0.clone()).handle_result(err) } /// Open a database given by a configuration. If no initialized database is present this procedure will fail. @@ -406,7 +389,7 @@ pub unsafe extern "C" fn betree_build_db(cfg: *const cfg_t, err: *mut *mut err_t pub unsafe extern "C" fn betree_open_db(cfg: *const cfg_t, err: *mut *mut err_t) -> *mut db_t { let mut db_cfg = (*cfg).0.clone(); db_cfg.access_mode = AccessMode::OpenIfExists; - Database::build_threaded(db_cfg).handle_result(err) + Database::build(db_cfg).handle_result(err) } /// Create a database given by a configuration. @@ -419,7 +402,7 @@ pub unsafe extern "C" fn betree_open_db(cfg: *const cfg_t, err: *mut *mut err_t) pub unsafe extern "C" fn betree_create_db(cfg: *const cfg_t, err: *mut *mut err_t) -> *mut db_t { let mut db_cfg = (*cfg).0.clone(); db_cfg.access_mode = AccessMode::AlwaysCreateNew; - Database::build_threaded(db_cfg).handle_result(err) + Database::build(db_cfg).handle_result(err) } /// Create a database given by a configuration. @@ -435,8 +418,7 @@ pub unsafe extern "C" fn betree_open_or_create_db( ) -> *mut db_t { let mut db_cfg = (*cfg).0.clone(); db_cfg.access_mode = AccessMode::OpenOrCreate; - // BUG: This returns a nullptr - Database::build_threaded(db_cfg).handle_result(err) + Database::build(db_cfg).handle_result(err) } /// Sync a database. diff --git a/betree/src/database/handler.rs b/betree/src/database/handler.rs index b1c23cef3..56c182b19 100644 --- a/betree/src/database/handler.rs +++ b/betree/src/database/handler.rs @@ -198,16 +198,16 @@ impl Handler { let mut allocator: Box = match self.allocator { AllocatorType::FirstFitScan => Box::new(FirstFitScan::new(bitmap)), AllocatorType::FirstFitList => Box::new(FirstFitList::new(bitmap)), - AllocatorType::FirstFitTree => Box::new(FirstFitTree::new(bitmap)), + AllocatorType::FirstFitTree => Box::new(FirstFitFSM::new(bitmap)), AllocatorType::NextFitScan => Box::new(NextFitScan::new(bitmap)), AllocatorType::NextFitList => Box::new(NextFitList::new(bitmap)), AllocatorType::BestFitScan => Box::new(BestFitScan::new(bitmap)), AllocatorType::BestFitList => Box::new(BestFitList::new(bitmap)), AllocatorType::BestFitTree => Box::new(BestFitTree::new(bitmap)), - AllocatorType::ApproximateBestFitTree => Box::new(ApproximateBestFitTree::new(bitmap)), + AllocatorType::ApproximateBestFitTree => Box::new(BestFitFSM::new(bitmap)), AllocatorType::WorstFitScan => Box::new(WorstFitScan::new(bitmap)), AllocatorType::WorstFitList => Box::new(WorstFitList::new(bitmap)), - AllocatorType::WorstFitTree => Box::new(WorstFitTree::new(bitmap)), + AllocatorType::WorstFitTree => Box::new(WorstFitFSM::new(bitmap)), AllocatorType::SegmentAllocator => Box::new(SegmentAllocator::new(bitmap)), AllocatorType::HybridAllocator => Box::new(HybridAllocator::new(bitmap)), };