diff --git a/mem_arena/src/lib.rs b/mem_arena/src/lib.rs index 1ef1e89..97ebe50 100644 --- a/mem_arena/src/lib.rs +++ b/mem_arena/src/lib.rs @@ -2,8 +2,9 @@ use std::slice; use std::cell::{Cell, RefCell}; use std::mem::{size_of, align_of}; -const DEFAULT_BLOCK_SIZE: usize = (1 << 20) * 32; // 32 MiB -const DEFAULT_LARGE_ALLOCATION_THRESHOLD: usize = 1 << 20; +const GROWTH_FRACTION: usize = 8; // 1/N (smaller number leads to bigger allocations) +const DEFAULT_MIN_BLOCK_SIZE: usize = 1 << 10; // 1 KiB +const DEFAULT_MAX_WASTE_PERCENTAGE: usize = 10; fn alignment_offset(addr: usize, alignment: usize) -> usize { (alignment - (addr % alignment)) % alignment @@ -11,50 +12,58 @@ fn alignment_offset(addr: usize, alignment: usize) -> usize { /// A growable memory arena for Copy types. /// -/// The arena works by allocating memory in blocks of a fixed size. It doles -/// out memory from the current block until an amount of memory is requested that -/// doesn't fit in the remainder of the current block, and then allocates a new +/// The arena works by allocating memory in blocks of slowly increasing size. It +/// doles out memory from the current block until an amount of memory is requested +/// that doesn't fit in the remainder of the current block, and then allocates a new /// block. /// -/// Additionally, to minimize unused space in blocks, allocations above a specified -/// size (the large allocation threshold) are given their own block if they won't -/// fit in the remainder of the current block. -/// -/// The block size and large allocation threshold are configurable. +/// Additionally, it attempts to minimize wasted space through some heuristics. By +/// default, it tries to keep memory waste within the arena below 10%. #[derive(Debug)] pub struct MemArena { blocks: RefCell>>, - block_size: usize, - large_alloc_threshold: usize, + min_block_size: usize, + max_waste_percentage: usize, stat_space_occupied: Cell, stat_space_allocated: Cell, - stat_large_blocks: Cell, } impl MemArena { - /// Create a new arena, with default block size and large allocation threshold. + /// Create a new arena, with default minimum block size. pub fn new() -> MemArena { MemArena { - blocks: RefCell::new(vec![Vec::with_capacity(DEFAULT_BLOCK_SIZE)]), - block_size: DEFAULT_BLOCK_SIZE, - large_alloc_threshold: DEFAULT_LARGE_ALLOCATION_THRESHOLD, - stat_space_occupied: Cell::new(DEFAULT_BLOCK_SIZE), + blocks: RefCell::new(vec![Vec::with_capacity(DEFAULT_MIN_BLOCK_SIZE)]), + min_block_size: DEFAULT_MIN_BLOCK_SIZE, + max_waste_percentage: DEFAULT_MAX_WASTE_PERCENTAGE, + stat_space_occupied: Cell::new(DEFAULT_MIN_BLOCK_SIZE), stat_space_allocated: Cell::new(0), - stat_large_blocks: Cell::new(0), } } - /// Create a new arena, with a custom block size and large allocation threshold. - pub fn new_with_settings(block_size: usize, large_alloc_threshold: usize) -> MemArena { - assert!(large_alloc_threshold <= block_size); + /// Create a new arena, with a specified minimum block size. + pub fn with_min_block_size(min_block_size: usize) -> MemArena { + assert!(min_block_size > 0); MemArena { - blocks: RefCell::new(vec![Vec::with_capacity(block_size)]), - block_size: block_size, - large_alloc_threshold: large_alloc_threshold, - stat_space_occupied: Cell::new(block_size), + blocks: RefCell::new(vec![Vec::with_capacity(min_block_size)]), + min_block_size: min_block_size, + max_waste_percentage: DEFAULT_MAX_WASTE_PERCENTAGE, + stat_space_occupied: Cell::new(min_block_size), + stat_space_allocated: Cell::new(0), + } + } + + /// Create a new arena, with a specified minimum block size and maximum waste percentage. + pub fn with_settings(min_block_size: usize, max_waste_percentage: usize) -> MemArena { + assert!(min_block_size > 0); + assert!(max_waste_percentage > 0 && max_waste_percentage <= 100); + + MemArena { + blocks: RefCell::new(vec![Vec::with_capacity(min_block_size)]), + min_block_size: min_block_size, + max_waste_percentage: max_waste_percentage, + stat_space_occupied: Cell::new(min_block_size), stat_space_allocated: Cell::new(0), - stat_large_blocks: Cell::new(0), } } @@ -69,17 +78,12 @@ impl MemArena { /// allocation requests made to the arena by client code. /// /// Block count is the number of blocks that have been allocated. - /// - /// Large block count is the number of blocks that have beem specifically - /// allocated for allocation requests that were above the large - /// allocation threshold. - pub fn stats(&self) -> (usize, usize, usize, usize) { + pub fn stats(&self) -> (usize, usize, usize) { let occupied = self.stat_space_occupied.get(); let allocated = self.stat_space_allocated.get(); let blocks = self.blocks.borrow().len(); - let large_blocks = self.stat_large_blocks.get(); - (occupied, allocated, blocks, large_blocks) + (occupied, allocated, blocks) } /// Frees all memory currently allocated by the arena, resetting itself to start @@ -92,11 +96,10 @@ impl MemArena { blocks.clear(); blocks.shrink_to_fit(); - blocks.push(Vec::with_capacity(self.block_size)); + blocks.push(Vec::with_capacity(self.min_block_size)); - self.stat_space_occupied.set(self.block_size); + self.stat_space_occupied.set(self.min_block_size); self.stat_space_allocated.set(0); - self.stat_large_blocks.set(0); } /// Allocates memory for and initializes a type T, returning a mutable reference to it. @@ -159,6 +162,8 @@ impl MemArena { /// Allocates space with a given size and alignment. /// + /// This is the work-horse code of the MemArena. + /// /// CAUTION: this returns uninitialized memory. Make sure to initialize the /// memory after calling. unsafe fn alloc_raw(&self, size: usize, alignment: usize) -> *mut u8 { @@ -168,7 +173,7 @@ impl MemArena { let mut blocks = self.blocks.borrow_mut(); - // If it's a zero-size allocation, just point to the beginning of the curent block. + // If it's a zero-size allocation, just point to the beginning of the current block. if size == 0 { return blocks.first_mut().unwrap().as_mut_ptr(); } @@ -189,12 +194,31 @@ impl MemArena { } // If it won't fit in the current block, create a new block and use that. else { + let next_size = if blocks.len() >= GROWTH_FRACTION { + let a = self.stat_space_occupied.get() / GROWTH_FRACTION; + let b = a % self.min_block_size; + if b > 0 { + a - b + self.min_block_size + } else { + a + } + } else { + self.min_block_size + }; + + let waste_percentage = { + let w1 = ((blocks[0].capacity() - blocks[0].len()) * 100) / + blocks[0].capacity(); + let w2 = ((self.stat_space_occupied.get() - self.stat_space_allocated.get()) * + 100) / self.stat_space_occupied.get(); + if w1 < w2 { w1 } else { w2 } + }; + // If it's a "large allocation", give it its own memory block. - if size > self.large_alloc_threshold { + if (size + alignment) > next_size || waste_percentage > self.max_waste_percentage { // Update stats self.stat_space_occupied .set(self.stat_space_occupied.get() + size + alignment - 1); - self.stat_large_blocks.set(self.stat_large_blocks.get() + 1); blocks.push(Vec::with_capacity(size + alignment - 1)); blocks.last_mut().unwrap().set_len(size + alignment - 1); @@ -208,9 +232,9 @@ impl MemArena { // Otherwise create a new shared block. else { // Update stats - self.stat_space_occupied.set(self.stat_space_occupied.get() + self.block_size); + self.stat_space_occupied.set(self.stat_space_occupied.get() + next_size); - blocks.push(Vec::with_capacity(self.block_size)); + blocks.push(Vec::with_capacity(next_size)); let block_count = blocks.len(); blocks.swap(0, block_count - 1); diff --git a/src/main.rs b/src/main.rs index bcdc5f9..f1204b9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -138,7 +138,7 @@ fn main() { if child.type_name() == "Scene" { println!("Building scene..."); - let arena = MemArena::new_with_settings((1 << 20) * 64, (1 << 20) * 4); + let arena = MemArena::with_min_block_size((1 << 20) * 4); let mut r = parse_scene(&arena, child).unwrap_or_else(|e| { e.print(&psy_contents); panic!("Parse error."); @@ -177,16 +177,27 @@ fn main() { } println!("\tWrote image in {:.3}s", t.tick()); - // Print memory stats if dev info is wanted. + // Print memory stats if stats are wanted. if args.flag_stats { let arena_stats = arena.stats(); + let mib_occupied = arena_stats.0 as f64 / 1048576.0; + let mib_allocated = arena_stats.1 as f64 / 1048576.0; + println!("MemArena stats:"); - println!("\tOccupied: {:.1} MiB", - arena_stats.0 as f64 / 1048576.0); - println!("\tAllocated: {:.1} MiB", - arena_stats.1 as f64 / 1048576.0); + + if mib_occupied >= 1.0 { + println!("\tOccupied: {:.1} MiB", mib_occupied); + } else { + println!("\tOccupied: {:.4} MiB", mib_occupied); + } + + if mib_allocated >= 1.0 { + println!("\tUsed: {:.1} MiB", mib_allocated); + } else { + println!("\tUsed: {:.4} MiB", mib_allocated); + } + println!("\tTotal blocks: {}", arena_stats.2); - println!("\tLarge blocks: {}", arena_stats.3); } } }