tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

bump_allocator.rs (15712B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 use std::{
      6    alloc::Layout,
      7    ptr::{self, NonNull}, sync::{Arc, Mutex},
      8 };
      9 
     10 use allocator_api2::alloc::{AllocError, Allocator, Global};
     11 
     12 const CHUNK_ALIGNMENT: usize = 32;
     13 const DEFAULT_CHUNK_SIZE: usize = 128 * 1024;
     14 
     15 /// A simple bump allocator, sub-allocating from fixed size chunks that are provided
     16 /// by a parent allocator.
     17 ///
     18 /// If an allocation is larger than the chunk size, a chunk sufficiently large to contain
     19 /// the allocation is added.
     20 pub struct BumpAllocator {
     21    /// The chunk we are currently allocating from.
     22    current_chunk: NonNull<Chunk>,
     23    /// For debugging.
     24    allocation_count: i32,
     25 
     26    chunk_pool: Arc<ChunkPool>,
     27 
     28    stats: Stats,
     29 }
     30 
     31 impl BumpAllocator {
     32    pub fn new(chunk_pool: Arc<ChunkPool>) -> Self {
     33        let mut stats = Stats::default();
     34 
     35        let first_chunk = chunk_pool.allocate_chunk(DEFAULT_CHUNK_SIZE).unwrap();
     36        stats.chunks = 1;
     37        stats.reserved_bytes += DEFAULT_CHUNK_SIZE;
     38 
     39        BumpAllocator {
     40            current_chunk: first_chunk,
     41            chunk_pool,
     42            allocation_count: 0,
     43            stats,
     44        }
     45    }
     46 
     47    pub fn get_stats(&mut self) -> Stats {
     48        self.stats.chunk_utilization = self.stats.chunks as f32 - 1.0 + Chunk::utilization(self.current_chunk);
     49        self.stats
     50    }
     51 
     52    pub fn allocate_item(&mut self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
     53        self.stats.allocations += 1;
     54        self.stats.allocated_bytes += layout.size();
     55 
     56        if let Ok(alloc) = Chunk::allocate_item(self.current_chunk, layout) {
     57            self.allocation_count += 1;
     58            return Ok(alloc);
     59        }
     60 
     61        self.alloc_chunk(layout.size())?;
     62 
     63        match Chunk::allocate_item(self.current_chunk, layout) {
     64            Ok(alloc) => {
     65                self.allocation_count += 1;
     66                    return Ok(alloc);
     67            }
     68            Err(_) => {
     69                return Err(AllocError);
     70            }
     71        }
     72    }
     73 
     74    pub fn deallocate_item(&mut self, ptr: NonNull<u8>, layout: Layout) {
     75        self.stats.deallocations += 1;
     76 
     77        if Chunk::contains_item(self.current_chunk, ptr) {
     78            unsafe { Chunk::deallocate_item(self.current_chunk, ptr, layout); }
     79        }
     80 
     81        self.allocation_count -= 1;
     82        debug_assert!(self.allocation_count >= 0);
     83    }
     84 
     85    pub unsafe fn grow_item(&mut self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
     86        debug_assert!(
     87            new_layout.size() >= old_layout.size(),
     88            "`new_layout.size()` must be greater than or equal to `old_layout.size()`"
     89        );
     90 
     91        self.stats.reallocations += 1;
     92 
     93        if Chunk::contains_item(self.current_chunk, ptr) {
     94            if let Ok(alloc) = Chunk::grow_item(self.current_chunk, ptr, old_layout, new_layout) {
     95                self.stats.in_place_reallocations += 1;
     96                return Ok(alloc);
     97            }
     98        }
     99 
    100        let new_alloc = if let Ok(alloc) = Chunk::allocate_item(self.current_chunk, new_layout) {
    101            alloc
    102        } else {
    103            self.alloc_chunk(new_layout.size())?;
    104            Chunk::allocate_item(self.current_chunk, new_layout).map_err(|_| AllocError)?
    105        };
    106 
    107        self.stats.reallocated_bytes += old_layout.size();
    108 
    109        unsafe {
    110            ptr::copy_nonoverlapping(ptr.as_ptr(), new_alloc.as_ptr().cast(), old_layout.size());
    111        }
    112 
    113        Ok(new_alloc)
    114    }
    115 
    116    pub unsafe fn shrink_item(&mut self, ptr: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
    117        debug_assert!(
    118            new_layout.size() <= old_layout.size(),
    119            "`new_layout.size()` must be smaller than or equal to `old_layout.size()`"
    120        );
    121 
    122        if Chunk::contains_item(self.current_chunk, ptr) {
    123            return unsafe { Ok(Chunk::shrink_item(self.current_chunk, ptr, old_layout, new_layout)) };
    124        }
    125 
    126        // Can't actually shrink, so return the full range of the previous allocation.
    127        Ok(NonNull::slice_from_raw_parts(ptr, old_layout.size()))
    128    }
    129 
    130    fn alloc_chunk(&mut self, item_size: usize) -> Result<(), AllocError> {
    131        let chunk_size = DEFAULT_CHUNK_SIZE.max(align(item_size, CHUNK_ALIGNMENT) + CHUNK_ALIGNMENT);
    132        self.stats.reserved_bytes += chunk_size;
    133 
    134        let chunk = self.chunk_pool.allocate_chunk(chunk_size)?;
    135 
    136        unsafe {
    137            (*chunk.as_ptr()).previous = Some(self.current_chunk);
    138        }
    139        self.current_chunk = chunk;
    140 
    141        self.stats.chunks += 1;
    142 
    143        Ok(())
    144    }
    145 }
    146 
    147 impl Drop for BumpAllocator {
    148    fn drop(&mut self) {
    149        assert!(self.allocation_count == 0);
    150 
    151        unsafe {
    152            self.chunk_pool.recycle_chunks(self.current_chunk);
    153        }
    154    }
    155 }
    156 
    157 /// A Contiguous buffer of memory holding multiple sub-allocaions.
    158 pub struct Chunk {
    159    previous: Option<NonNull<Chunk>>,
    160    /// Offset of the next allocation
    161    cursor: *mut u8,
    162    /// Points to the first byte after the chunk's buffer.
    163    chunk_end: *mut u8,
    164    /// Size of the chunk
    165    size: usize,
    166 }
    167 
    168 impl Chunk {
    169    pub fn allocate_item(this: NonNull<Chunk>, layout: Layout) -> Result<NonNull<[u8]>, ()> {
    170        // Common wisdom would be to always bump address downward (https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html).
    171        // However, bump allocation does not show up in profiles with the current workloads
    172        // so we can keep things simple for now.
    173        debug_assert!(CHUNK_ALIGNMENT % layout.align() == 0);
    174        debug_assert!(layout.align() > 0);
    175        debug_assert!(layout.align().is_power_of_two());
    176 
    177        let size = align(layout.size(), CHUNK_ALIGNMENT);
    178 
    179        unsafe {
    180            let cursor = (*this.as_ptr()).cursor;
    181            let end = (*this.as_ptr()).chunk_end;
    182            let available_size = end.offset_from(cursor);
    183 
    184            if size as isize > available_size {
    185                return Err(());
    186            }
    187 
    188            let next = cursor.add(size);
    189 
    190            (*this.as_ptr()).cursor = next;
    191 
    192            let cursor = NonNull::new(cursor).unwrap();
    193            let suballocation: NonNull<[u8]> = NonNull::slice_from_raw_parts(cursor, size);
    194 
    195            Ok(suballocation)
    196        }
    197    }
    198 
    199    pub unsafe fn deallocate_item(this: NonNull<Chunk>, item: NonNull<u8>, layout: Layout) {
    200        debug_assert!(Chunk::contains_item(this, item));
    201 
    202        unsafe {
    203            let size = align(layout.size(), CHUNK_ALIGNMENT);
    204            let item_end = item.as_ptr().add(size);
    205 
    206            // If the item is the last allocation, then move the cursor back
    207            // to reuse its memory.
    208            if item_end == (*this.as_ptr()).cursor {
    209                (*this.as_ptr()).cursor = item.as_ptr();
    210            }
    211 
    212            // Otherwise, deallocation is a no-op
    213        }
    214    }
    215 
    216    pub unsafe fn grow_item(this: NonNull<Chunk>, item: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> Result<NonNull<[u8]>, ()> {
    217        debug_assert!(Chunk::contains_item(this, item));
    218 
    219        let old_size = align(old_layout.size(), CHUNK_ALIGNMENT);
    220        let new_size = align(new_layout.size(), CHUNK_ALIGNMENT);
    221        let old_item_end = item.as_ptr().add(old_size);
    222 
    223        if old_item_end != (*this.as_ptr()).cursor {
    224            return Err(());
    225        }
    226 
    227        // The item is the last allocation. we can attempt to just move
    228        // the cursor if the new size fits.
    229 
    230        let chunk_end = (*this.as_ptr()).chunk_end;
    231        let available_size = chunk_end.offset_from(item.as_ptr());
    232 
    233        if new_size as isize > available_size {
    234            // Does not fit.
    235            return Err(());
    236        }
    237 
    238        let new_item_end = item.as_ptr().add(new_size);
    239        (*this.as_ptr()).cursor = new_item_end;
    240 
    241        Ok(NonNull::slice_from_raw_parts(item, new_size))
    242    }
    243 
    244    pub unsafe fn shrink_item(this: NonNull<Chunk>, item: NonNull<u8>, old_layout: Layout, new_layout: Layout) -> NonNull<[u8]> {
    245        debug_assert!(Chunk::contains_item(this, item));
    246 
    247        let old_size = align(old_layout.size(), CHUNK_ALIGNMENT);
    248        let new_size = align(new_layout.size(), CHUNK_ALIGNMENT);
    249        let old_item_end = item.as_ptr().add(old_size);
    250 
    251        // The item is the last allocation. we can attempt to just move
    252        // the cursor if the new size fits.
    253 
    254        if old_item_end == (*this.as_ptr()).cursor {
    255            let new_item_end = item.as_ptr().add(new_size);
    256            (*this.as_ptr()).cursor = new_item_end;
    257        }
    258 
    259        NonNull::slice_from_raw_parts(item, new_size)
    260    }
    261 
    262    pub fn contains_item(this: NonNull<Chunk>, item: NonNull<u8>) -> bool {
    263        unsafe {
    264            let start: *mut u8 = this.cast::<u8>().as_ptr().add(CHUNK_ALIGNMENT);
    265            let end: *mut u8 = (*this.as_ptr()).chunk_end;
    266            let item = item.as_ptr();
    267 
    268            start <= item && item < end
    269        }
    270    }
    271 
    272    fn available_size(this: NonNull<Chunk>) -> usize {
    273        unsafe {
    274            let this = this.as_ptr();
    275            (*this).chunk_end.offset_from((*this).cursor) as usize
    276        }
    277    }
    278 
    279    fn utilization(this: NonNull<Chunk>) -> f32 {
    280        let size = unsafe { (*this.as_ptr()).size } as f32;
    281        (size - Chunk::available_size(this) as f32) / size
    282    }
    283 }
    284 
    285 fn align(val: usize, alignment: usize) -> usize {
    286    let rem = val % alignment;
    287    if rem == 0 {
    288        return val;
    289    }
    290 
    291    val.checked_add(alignment).unwrap() - rem
    292 }
    293 
    294 #[derive(Copy, Clone, Debug, Default)]
    295 pub struct Stats {
    296    pub chunks: u32,
    297    pub chunk_utilization: f32,
    298    pub allocations: u32,
    299    pub deallocations: u32,
    300    pub reallocations: u32,
    301    pub in_place_reallocations: u32,
    302 
    303    pub reallocated_bytes: usize,
    304    pub allocated_bytes: usize,
    305    pub reserved_bytes: usize,
    306 }
    307 
    308 /// A simple pool for allocating and recycling memory chunks of a fixed size,
    309 /// protected by a mutex.
    310 ///
    311 /// Chunks in the pool are stored as a linked list using a pointer to the next
    312 /// element at the beginning of the chunk.
    313 pub struct ChunkPool {
    314    inner: Mutex<ChunkPoolInner>,
    315 }
    316 
    317 struct ChunkPoolInner {
    318    first: Option<NonNull<RecycledChunk>>,
    319    count: i32,
    320 }
    321 
    322 /// Header at the beginning of recycled memory chunk.
    323 struct RecycledChunk {
    324    next: Option<NonNull<RecycledChunk>>,
    325 }
    326 
    327 impl ChunkPool {
    328    pub fn new() -> Self {
    329        ChunkPool {
    330            inner: Mutex::new(ChunkPoolInner {
    331                first: None,
    332                count: 0,
    333            }),
    334        }
    335    }
    336 
    337    /// Pop a chunk from the pool or allocate a new one.
    338    ///
    339    /// If the requested size is not equal to the default chunk size,
    340    /// a new chunk is allocated.
    341    pub fn allocate_chunk(&self, size: usize) -> Result<NonNull<Chunk>, AllocError> {
    342        let chunk: Option<NonNull<RecycledChunk>> = if size == DEFAULT_CHUNK_SIZE {
    343            // Try to reuse a chunk.
    344            let mut inner = self.inner.lock().unwrap();
    345            let mut chunk = inner.first.take();
    346            inner.first = chunk.as_mut().and_then(|chunk| unsafe { chunk.as_mut().next.take() });
    347 
    348            if chunk.is_some() {
    349                inner.count -= 1;
    350                debug_assert!(inner.count >= 0);
    351            }
    352 
    353            chunk
    354        } else {
    355            // Always allocate a new chunk if it is not the standard size.
    356            None
    357        };
    358 
    359        let chunk: NonNull<Chunk> = match chunk {
    360            Some(chunk) => chunk.cast(),
    361            None => {
    362                // Allocate a new one.
    363                let layout = match Layout::from_size_align(size, CHUNK_ALIGNMENT) {
    364                    Ok(layout) => layout,
    365                    Err(_) => {
    366                        return Err(AllocError);
    367                    }
    368                };
    369 
    370                let alloc = Global.allocate(layout)?;
    371 
    372                alloc.cast()
    373            }
    374        };
    375 
    376        let chunk_start: *mut u8 = chunk.cast().as_ptr();
    377 
    378        unsafe {
    379            let chunk_end = chunk_start.add(size);
    380            let cursor = chunk_start.add(CHUNK_ALIGNMENT);
    381            ptr::write(
    382                chunk.as_ptr(),
    383                Chunk {
    384                    previous: None,
    385                    chunk_end,
    386                    cursor,
    387                    size,
    388                },
    389            );
    390        }
    391 
    392        Ok(chunk)
    393    }
    394 
    395    /// Put the provided list of chunks into the pool.
    396    ///
    397    /// Chunks with size different from the default chunk size are deallocated
    398    /// immediately.
    399    ///
    400    /// # Safety
    401    ///
    402    /// Ownership of the provided chunks is transfered to the pool, nothing
    403    /// else can access them after this function runs.
    404    unsafe fn recycle_chunks(&self, chunk: NonNull<Chunk>) {
    405        let mut inner = self.inner.lock().unwrap();
    406        let mut iter = Some(chunk);
    407        // Go through the provided linked list of chunks, and insert each
    408        // of them at the beginning of our linked list of recycled chunks.
    409        while let Some(mut chunk) = iter {
    410            // Advance the iterator.
    411            iter = unsafe { chunk.as_mut().previous.take() };
    412 
    413            unsafe {
    414                // Don't recycle chunks with a non-standard size.
    415                let size = chunk.as_ref().size;
    416                if size != DEFAULT_CHUNK_SIZE {
    417                    let layout = Layout::from_size_align(size, CHUNK_ALIGNMENT).unwrap();
    418                    Global.deallocate(chunk.cast(), layout);
    419                    continue;
    420                }
    421            }
    422 
    423            // Turn the chunk into a recycled chunk.
    424            let recycled: NonNull<RecycledChunk> = chunk.cast();
    425 
    426            // Insert into the recycled list.
    427            unsafe {
    428                ptr::write(recycled.as_ptr(), RecycledChunk {
    429                    next: inner.first,
    430                });
    431            }
    432            inner.first = Some(recycled);
    433 
    434            inner.count += 1;
    435        }
    436    }
    437 
    438    /// Deallocate chunks until the pool contains at most `target` items, or
    439    /// `count` chunks have been deallocated.
    440    ///
    441    /// Returns `true` if the target number of chunks in the pool was reached,
    442    /// `false` if this method stopped before reaching the target.
    443    ///
    444    /// Purging chunks can be expensive so it is preferable to perform this
    445    /// operation outside of the critical path. Specifying a lower `count`
    446    /// allows the caller to split the work and spread it over time.
    447    #[inline(never)]
    448    pub fn purge_chunks(&self, target: u32, mut count: u32) -> bool {
    449        let mut inner = self.inner.lock().unwrap();
    450        assert!(inner.count >= 0);
    451 
    452        while inner.count as u32 > target {
    453            unsafe {
    454                // First can't be None because inner.count > 0.
    455                let chunk = inner.first.unwrap();
    456 
    457                // Pop chunk off the list.
    458                inner.first = chunk.as_ref().next;
    459 
    460                // Deallocate chunk.
    461                let layout = Layout::from_size_align(
    462                    DEFAULT_CHUNK_SIZE,
    463                    CHUNK_ALIGNMENT
    464                ).unwrap();
    465                Global.deallocate(chunk.cast(), layout);
    466            }
    467 
    468            inner.count -= 1;
    469            count -= 1;
    470 
    471            if count == 0 {
    472                return false;
    473            }
    474        }
    475 
    476        return true;
    477    }
    478 
    479    /// Deallocate all of the chunks.
    480    pub fn purge_all_chunks(&self) {
    481        self.purge_chunks(0, u32::MAX);
    482    }
    483 }
    484 
    485 impl Drop for ChunkPool {
    486    fn drop(&mut self) {
    487        self.purge_all_chunks();
    488    }
    489 }
    490 
    491 unsafe impl Send for ChunkPoolInner {}