|
2 | 2 | //!
|
3 | 3 | //! A memory region that contains a single object and header
|
4 | 4 |
|
| 5 | +use std::alloc::{alloc, dealloc, Layout}; |
5 | 6 | use std::process::abort;
|
6 |
| - |
7 |
| -use super::block::Block; |
| 7 | +use std::ptr::NonNull; |
8 | 8 |
|
9 | 9 | /// A memory allocation containing a single large object with its
|
10 | 10 | /// header, this differs from Block in that it needn't be a power of
|
11 | 11 | /// two.
|
12 | 12 | #[derive(Debug)]
|
13 | 13 | pub struct LargeObjectBlock {
|
14 |
| - /// Block |
15 |
| - block: Block, |
| 14 | + /// Pointer to memory |
| 15 | + ptr: NonNull<u8>, |
| 16 | + /// Size of allocation |
| 17 | + size: usize, |
16 | 18 | }
|
17 | 19 |
|
18 | 20 | impl LargeObjectBlock {
|
19 | 21 | /// Create a new LargeObjectBlock of size sufficient to contain
|
20 |
| - /// `required_size` bytes (but potentially much larger). The size |
21 |
| - /// of any object headers is assumed to be already included. |
| 22 | + /// `required_size` bytes. Uses efficient sizing to minimize waste |
| 23 | + /// while maintaining reasonable allocation granularity. |
22 | 24 | pub fn new(required_size: usize) -> Self {
|
23 |
| - // TODO: extraordinarily wasteful! |
24 |
| - let size = required_size.next_power_of_two(); |
| 25 | + let size = Self::efficient_size_for(required_size); |
25 | 26 | LargeObjectBlock {
|
26 |
| - block: Block::new(size).unwrap_or_else(|_| abort()), |
| 27 | + ptr: Self::alloc_block(size).unwrap_or_else(|_| abort()), |
| 28 | + size, |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + /// Allocate a block directly from the system allocator |
| 33 | + fn alloc_block(size: usize) -> Result<NonNull<u8>, ()> { |
| 34 | + unsafe { |
| 35 | + // Use page alignment for better performance |
| 36 | + let align = std::cmp::max(size.next_power_of_two().min(4096), 8); |
| 37 | + let layout = Layout::from_size_align(size, align).map_err(|_| ())?; |
| 38 | + let ptr = alloc(layout); |
| 39 | + if ptr.is_null() { |
| 40 | + Err(()) |
| 41 | + } else { |
| 42 | + if cfg!(debug_assertions) { |
| 43 | + // Fill memory with 0xff to aid debugging |
| 44 | + let mem = std::slice::from_raw_parts_mut(ptr, size); |
| 45 | + mem.fill(0xff); |
| 46 | + } |
| 47 | + Ok(NonNull::new_unchecked(ptr)) |
| 48 | + } |
| 49 | + } |
| 50 | + } |
| 51 | + |
| 52 | + /// Calculate efficient allocation size that minimizes waste while maintaining |
| 53 | + /// reasonable granularity for the underlying allocator. |
| 54 | + /// |
| 55 | + /// Uses a tiered approach: |
| 56 | + /// - Up to 128KB: round to next 16KB boundary (max 15KB waste = ~12%) |
| 57 | + /// - Up to 1MB: round to next 64KB boundary (max 63KB waste = ~6%) |
| 58 | + /// - Above 1MB: round to next 256KB boundary (max 255KB waste = ~25% max, but rare) |
| 59 | + fn efficient_size_for(required_size: usize) -> usize { |
| 60 | + const KB: usize = 1024; |
| 61 | + const MB: usize = 1024 * KB; |
| 62 | + |
| 63 | + if required_size <= 128 * KB { |
| 64 | + // Round up to next 16KB boundary |
| 65 | + required_size.div_ceil(16 * KB) * (16 * KB) |
| 66 | + } else if required_size <= MB { |
| 67 | + // Round up to next 64KB boundary |
| 68 | + required_size.div_ceil(64 * KB) * (64 * KB) |
| 69 | + } else { |
| 70 | + // Round up to next 256KB boundary |
| 71 | + required_size.div_ceil(256 * KB) * (256 * KB) |
27 | 72 | }
|
28 | 73 | }
|
29 | 74 |
|
30 | 75 | /// Pointer to the writeable memory area
|
31 | 76 | pub fn space(&self) -> *const u8 {
|
32 |
| - self.block.as_ptr() |
| 77 | + self.ptr.as_ptr() |
| 78 | + } |
| 79 | + |
| 80 | + /// Get the actual allocated size of this large object block |
| 81 | + pub fn allocated_size(&self) -> usize { |
| 82 | + self.size |
| 83 | + } |
| 84 | + |
| 85 | + /// Check if this block can accommodate the requested size |
| 86 | + pub fn can_fit(&self, required_size: usize) -> bool { |
| 87 | + self.size >= required_size |
| 88 | + } |
| 89 | + |
| 90 | + /// Calculate waste percentage for a given required size |
| 91 | + pub fn waste_percentage(&self, required_size: usize) -> f64 { |
| 92 | + if required_size == 0 { |
| 93 | + 100.0 |
| 94 | + } else { |
| 95 | + let waste = self.size.saturating_sub(required_size); |
| 96 | + (waste as f64 / self.size as f64) * 100.0 |
| 97 | + } |
| 98 | + } |
| 99 | +} |
| 100 | + |
| 101 | +impl Drop for LargeObjectBlock { |
| 102 | + fn drop(&mut self) { |
| 103 | + unsafe { |
| 104 | + let align = std::cmp::max(self.size.next_power_of_two().min(4096), 8); |
| 105 | + let layout = Layout::from_size_align_unchecked(self.size, align); |
| 106 | + dealloc(self.ptr.as_ptr(), layout); |
| 107 | + } |
| 108 | + } |
| 109 | +} |
| 110 | + |
| 111 | +#[cfg(test)] |
| 112 | +mod tests { |
| 113 | + use super::*; |
| 114 | + |
| 115 | + #[test] |
| 116 | + fn test_efficient_size_for_small_range() { |
| 117 | + // Test 16KB boundaries for sizes up to 128KB |
| 118 | + |
| 119 | + // Exact boundaries should not change |
| 120 | + assert_eq!(LargeObjectBlock::efficient_size_for(16 * 1024), 16 * 1024); |
| 121 | + assert_eq!(LargeObjectBlock::efficient_size_for(32 * 1024), 32 * 1024); |
| 122 | + assert_eq!(LargeObjectBlock::efficient_size_for(128 * 1024), 128 * 1024); |
| 123 | + |
| 124 | + // Values just above boundaries should round up |
| 125 | + assert_eq!(LargeObjectBlock::efficient_size_for(33 * 1024), 48 * 1024); |
| 126 | + assert_eq!(LargeObjectBlock::efficient_size_for(50 * 1024), 64 * 1024); |
| 127 | + assert_eq!(LargeObjectBlock::efficient_size_for(65 * 1024), 80 * 1024); |
| 128 | + } |
| 129 | + |
| 130 | + #[test] |
| 131 | + fn test_efficient_size_for_medium_range() { |
| 132 | + // Test 64KB boundaries for 128KB < size <= 1MB |
| 133 | + |
| 134 | + // Exact boundaries |
| 135 | + assert_eq!(LargeObjectBlock::efficient_size_for(192 * 1024), 192 * 1024); |
| 136 | + assert_eq!(LargeObjectBlock::efficient_size_for(256 * 1024), 256 * 1024); |
| 137 | + assert_eq!( |
| 138 | + LargeObjectBlock::efficient_size_for(1024 * 1024), |
| 139 | + 1024 * 1024 |
| 140 | + ); |
| 141 | + |
| 142 | + // Round up cases |
| 143 | + assert_eq!(LargeObjectBlock::efficient_size_for(129 * 1024), 192 * 1024); |
| 144 | + assert_eq!(LargeObjectBlock::efficient_size_for(200 * 1024), 256 * 1024); |
| 145 | + assert_eq!(LargeObjectBlock::efficient_size_for(900 * 1024), 960 * 1024); |
| 146 | + // 15 * 64KB |
| 147 | + } |
| 148 | + |
| 149 | + #[test] |
| 150 | + fn test_efficient_size_for_large_range() { |
| 151 | + // Test 256KB boundaries for size > 1MB |
| 152 | + |
| 153 | + // Exact boundaries |
| 154 | + assert_eq!( |
| 155 | + LargeObjectBlock::efficient_size_for(1280 * 1024), |
| 156 | + 1280 * 1024 |
| 157 | + ); |
| 158 | + assert_eq!( |
| 159 | + LargeObjectBlock::efficient_size_for(2048 * 1024), |
| 160 | + 2048 * 1024 |
| 161 | + ); |
| 162 | + |
| 163 | + // Round up cases |
| 164 | + assert_eq!( |
| 165 | + LargeObjectBlock::efficient_size_for(1025 * 1024), |
| 166 | + 1280 * 1024 |
| 167 | + ); |
| 168 | + assert_eq!( |
| 169 | + LargeObjectBlock::efficient_size_for(1500 * 1024), |
| 170 | + 1536 * 1024 |
| 171 | + ); |
| 172 | + } |
| 173 | + |
| 174 | + #[test] |
| 175 | + fn test_waste_percentage_calculation() { |
| 176 | + let lob = LargeObjectBlock::new(50 * 1024); // Should allocate 64KB |
| 177 | + |
| 178 | + // Perfect fit should have some waste due to rounding |
| 179 | + let waste = lob.waste_percentage(50 * 1024); |
| 180 | + assert!(waste > 0.0); |
| 181 | + assert!(waste < 30.0); // Should be reasonable |
| 182 | + |
| 183 | + // Very small allocation in large block should have high waste |
| 184 | + let high_waste = lob.waste_percentage(1024); |
| 185 | + assert!(high_waste > 90.0); |
| 186 | + |
| 187 | + // Zero size should be 100% waste |
| 188 | + assert_eq!(lob.waste_percentage(0), 100.0); |
| 189 | + } |
| 190 | + |
| 191 | + #[test] |
| 192 | + fn test_can_fit() { |
| 193 | + let lob = LargeObjectBlock::new(50 * 1024); // Allocates 64KB |
| 194 | + |
| 195 | + assert!(lob.can_fit(32 * 1024)); |
| 196 | + assert!(lob.can_fit(50 * 1024)); |
| 197 | + assert!(lob.can_fit(64 * 1024)); |
| 198 | + assert!(!lob.can_fit(65 * 1024)); |
| 199 | + assert!(!lob.can_fit(100 * 1024)); |
| 200 | + } |
| 201 | + |
| 202 | + #[test] |
| 203 | + fn test_size_efficiency_vs_power_of_two() { |
| 204 | + // Compare our efficient sizing vs power-of-two for various sizes |
| 205 | + let test_sizes = [ |
| 206 | + 33 * 1024, // 33KB |
| 207 | + 50 * 1024, // 50KB |
| 208 | + 100 * 1024, // 100KB |
| 209 | + 200 * 1024, // 200KB |
| 210 | + 500 * 1024, // 500KB |
| 211 | + ]; |
| 212 | + |
| 213 | + for &size in &test_sizes { |
| 214 | + let efficient = LargeObjectBlock::efficient_size_for(size); |
| 215 | + let power_of_two = size.next_power_of_two(); |
| 216 | + |
| 217 | + // Our efficient sizing should always be <= power of two |
| 218 | + assert!(efficient <= power_of_two); |
| 219 | + |
| 220 | + // For most sizes, we should be significantly more efficient |
| 221 | + if size > 32 * 1024 { |
| 222 | + let efficient_waste = (efficient - size) as f64 / efficient as f64; |
| 223 | + let power_waste = (power_of_two - size) as f64 / power_of_two as f64; |
| 224 | + |
| 225 | + // Our algorithm should generally produce less waste |
| 226 | + assert!(efficient_waste <= power_waste); |
| 227 | + } |
| 228 | + } |
33 | 229 | }
|
34 | 230 | }
|
0 commit comments