mmtk/util/alloc/
free_list_allocator.rs

1// This is a free list allocator written based on Microsoft's mimalloc allocator https://www.microsoft.com/en-us/research/publication/mimalloc-free-list-sharding-in-action/
2
3use std::sync::Arc;
4
5use super::allocator::AllocatorContext;
6use crate::policy::marksweepspace::native_ms::*;
7use crate::util::alloc::allocator;
8use crate::util::alloc::Allocator;
9use crate::util::linear_scan::Region;
10use crate::util::Address;
11use crate::util::VMThread;
12use crate::vm::VMBinding;
13
14/// A MiMalloc free list allocator
15#[repr(C)]
16pub struct FreeListAllocator<VM: VMBinding> {
17    /// [`VMThread`] associated with this allocator instance
18    pub tls: VMThread,
19    space: &'static MarkSweepSpace<VM>,
20    context: Arc<AllocatorContext<VM>>,
21    /// blocks with free space
22    pub available_blocks: BlockLists,
23    /// blocks with free space for precise stress GC
24    /// For precise stress GC, we need to be able to trigger slowpath allocation for
25    /// each allocation. To achieve this, we put available blocks to this list. So
26    /// normal fastpath allocation will fail, as they will see the block lists
27    /// as empty.
28    pub available_blocks_stress: BlockLists,
29    /// blocks that are marked, not swept
30    pub unswept_blocks: BlockLists,
31    /// full blocks
32    pub consumed_blocks: BlockLists,
33}
34
35impl<VM: VMBinding> Allocator<VM> for FreeListAllocator<VM> {
36    fn get_tls(&self) -> VMThread {
37        self.tls
38    }
39
40    fn get_space(&self) -> &'static dyn crate::policy::space::Space<VM> {
41        self.space
42    }
43
44    fn get_context(&self) -> &AllocatorContext<VM> {
45        &self.context
46    }
47
48    // Find a block with free space and allocate to it
49    fn alloc(&mut self, size: usize, align: usize, offset: usize) -> Address {
50        debug_assert!(
51            size <= MAX_BIN_SIZE,
52            "Alloc request for {} bytes is too big.",
53            size
54        );
55        debug_assert!(align <= VM::MAX_ALIGNMENT);
56        debug_assert!(align >= VM::MIN_ALIGNMENT);
57
58        if let Some(block) = self.find_free_block_local(size, align) {
59            let cell = self.block_alloc(block);
60            if !cell.is_zero() {
61                // We succeeded in fastpath alloc, this cannot be precise stress test
62                debug_assert!(
63                    !(*self.context.options.precise_stress
64                        && self.context.options.is_stress_test_gc_enabled())
65                );
66
67                let res = allocator::align_allocation::<VM>(cell, align, offset);
68                // Make sure that the allocation region is within the cell
69                #[cfg(debug_assertions)]
70                {
71                    let cell_size = block.load_block_cell_size();
72                    debug_assert!(
73                        res + size <= cell + cell_size,
74                        "Allocating (size = {}, align = {}, offset = {}) to the cell {} of size {}, but the end of the allocation region {} is beyond the cell end {}",
75                        size, align, offset, cell, cell_size, res + size, cell + cell_size
76                    );
77                }
78                return res;
79            }
80        }
81
82        self.alloc_slow(size, align, offset)
83    }
84
85    fn alloc_slow_once(&mut self, size: usize, align: usize, offset: usize) -> Address {
86        // Try get a block from the space
87        if let Some(block) = self.acquire_global_block(size, align, false) {
88            let addr = self.block_alloc(block);
89            allocator::align_allocation::<VM>(addr, align, offset)
90        } else {
91            Address::ZERO
92        }
93    }
94
95    fn does_thread_local_allocation(&self) -> bool {
96        true
97    }
98
99    fn get_thread_local_buffer_granularity(&self) -> usize {
100        Block::BYTES
101    }
102
103    fn alloc_slow_once_precise_stress(
104        &mut self,
105        size: usize,
106        align: usize,
107        offset: usize,
108        need_poll: bool,
109    ) -> Address {
110        trace!("allow slow precise stress s={}", size);
111        if need_poll {
112            self.acquire_global_block(0, 0, true);
113        }
114
115        // mimic what fastpath allocation does, except that we allocate from available_blocks_stress.
116        if let Some(block) = self.find_free_block_stress(size, align) {
117            let cell = self.block_alloc(block);
118            allocator::align_allocation::<VM>(cell, align, offset)
119        } else {
120            Address::ZERO
121        }
122    }
123
124    fn on_mutator_destroy(&mut self) {
125        let mut global = self.space.get_abandoned_block_lists().lock().unwrap();
126        self.abandon_blocks(&mut global);
127    }
128}
129
130impl<VM: VMBinding> FreeListAllocator<VM> {
131    // New free list allcoator
132    pub(crate) fn new(
133        tls: VMThread,
134        space: &'static MarkSweepSpace<VM>,
135        context: Arc<AllocatorContext<VM>>,
136    ) -> Self {
137        FreeListAllocator {
138            tls,
139            space,
140            context,
141            available_blocks: new_empty_block_lists(),
142            available_blocks_stress: new_empty_block_lists(),
143            unswept_blocks: new_empty_block_lists(),
144            consumed_blocks: new_empty_block_lists(),
145        }
146    }
147
148    // Find a free cell within a given block
149    fn block_alloc(&mut self, block: Block) -> Address {
150        let cell = block.load_free_list();
151        if cell.is_zero() {
152            return cell; // return failed allocation
153        }
154        let next_cell = unsafe { cell.load::<Address>() };
155        // Clear the link
156        unsafe { cell.store::<Address>(Address::ZERO) };
157        debug_assert!(
158            next_cell.is_zero() || block.includes_address(next_cell),
159            "next_cell {} is not in {:?}",
160            next_cell,
161            block
162        );
163        block.store_free_list(next_cell);
164
165        // Zeroing memory right before we return it.
166        // If we move the zeroing to somewhere else, we need to clear the list link here: cell.store::<Address>(Address::ZERO)
167        let cell_size = block.load_block_cell_size();
168        crate::util::memory::zero(cell, cell_size);
169
170        // Make sure the memory is zeroed. This looks silly as we zero the cell right before this check.
171        // But we would need to move the zeroing to somewhere so we can do zeroing at a coarser grainularity.
172        #[cfg(debug_assertions)]
173        {
174            let mut cursor = cell;
175            while cursor < cell + cell_size {
176                debug_assert_eq!(unsafe { cursor.load::<usize>() }, 0);
177                cursor += crate::util::constants::BYTES_IN_ADDRESS;
178            }
179        }
180
181        cell
182    }
183
184    // Find an available block when stress GC is enabled. This includes getting a block from the space.
185    fn find_free_block_stress(&mut self, size: usize, align: usize) -> Option<Block> {
186        Self::find_free_block_with(
187            &mut self.available_blocks_stress,
188            &mut self.consumed_blocks,
189            size,
190            align,
191        )
192        .or_else(|| self.recycle_local_blocks(size, align, true))
193        .or_else(|| self.acquire_global_block(size, align, true))
194    }
195
196    // Find an available block from local block lists
197    fn find_free_block_local(&mut self, size: usize, align: usize) -> Option<Block> {
198        Self::find_free_block_with(
199            &mut self.available_blocks,
200            &mut self.consumed_blocks,
201            size,
202            align,
203        )
204        .or_else(|| self.recycle_local_blocks(size, align, false))
205    }
206
207    // Find an available block
208    // This will usually be the first block on the available list. If all available blocks are found
209    // to be full, other lists are searched
210    // This function allows different available block lists -- normal allocation uses self.avaialble_blocks, and precise stress test uses self.avialable_blocks_stress.
211    fn find_free_block_with(
212        available_blocks: &mut BlockLists,
213        consumed_blocks: &mut BlockLists,
214        size: usize,
215        align: usize,
216    ) -> Option<Block> {
217        let bin = mi_bin::<VM>(size, align);
218        debug_assert!(bin <= MAX_BIN);
219
220        let available = &mut available_blocks[bin];
221        debug_assert!(available.size >= size);
222
223        if !available.is_empty() {
224            let mut cursor = available.first;
225
226            while let Some(block) = cursor {
227                if block.has_free_cells() {
228                    return Some(block);
229                }
230                available.pop();
231                consumed_blocks.get_mut(bin).unwrap().push(block);
232
233                cursor = available.first;
234            }
235        }
236
237        debug_assert!(available_blocks[bin].is_empty());
238        None
239    }
240
241    /// Add a block to the given bin in the available block lists. Depending on which available block list we are using, this
242    /// method may add the block to available_blocks, or available_blocks_stress.
243    fn add_to_available_blocks(&mut self, bin: usize, block: Block, stress: bool) {
244        if stress {
245            debug_assert!(*self.context.options.precise_stress);
246            self.available_blocks_stress[bin].push(block);
247        } else {
248            self.available_blocks[bin].push(block);
249        }
250    }
251
252    /// Tries to recycle local blocks if there is any. This is a no-op for eager sweeping mark sweep.
253    fn recycle_local_blocks(
254        &mut self,
255        size: usize,
256        align: usize,
257        _stress_test: bool,
258    ) -> Option<Block> {
259        if cfg!(feature = "eager_sweeping") {
260            // We have swept blocks in the last GC. If we run out of available blocks, there is nothing we can do.
261            None
262        } else {
263            // Get blocks from unswept_blocks and attempt to sweep
264            loop {
265                let bin = mi_bin::<VM>(size, align);
266                debug_assert!(self.available_blocks[bin].is_empty()); // only use this function if there are no blocks available
267
268                let block = self.unswept_blocks.get_mut(bin).unwrap().pop()?;
269
270                block.sweep::<VM>();
271                if block.has_free_cells() {
272                    // recyclable block
273                    self.add_to_available_blocks(
274                        bin,
275                        block,
276                        self.context.options.is_stress_test_gc_enabled(),
277                    );
278                    return Some(block);
279                } else {
280                    // nothing was freed from this block
281                    self.consumed_blocks.get_mut(bin).unwrap().push(block);
282                }
283            }
284        }
285    }
286
287    /// Get a block from the space.
288    fn acquire_global_block(
289        &mut self,
290        size: usize,
291        align: usize,
292        stress_test: bool,
293    ) -> Option<Block> {
294        let bin = mi_bin::<VM>(size, align);
295        loop {
296            match self.space.acquire_block(self.tls, size, align, self.get_context().get_alloc_options()) {
297                crate::policy::marksweepspace::native_ms::BlockAcquireResult::Exhausted => {
298                    debug!("Acquire global block: None");
299                    // GC
300                    return None;
301                }
302
303                crate::policy::marksweepspace::native_ms::BlockAcquireResult::Fresh(block) => {
304                    debug!("Acquire global block: Fresh {:?}", block);
305                    self.add_to_available_blocks(bin, block, stress_test);
306                    self.init_block(block, self.available_blocks[bin].size);
307
308                    return Some(block);
309                }
310
311                crate::policy::marksweepspace::native_ms::BlockAcquireResult::AbandonedAvailable(block) => {
312                    debug!("Acquire global block: AbandonedAvailable {:?}", block);
313                    block.store_tls(self.tls);
314                    if block.has_free_cells() {
315                        self.add_to_available_blocks(bin, block, stress_test);
316                        return Some(block);
317                    } else {
318                        self.consumed_blocks[bin].push(block);
319                    }
320                }
321
322                crate::policy::marksweepspace::native_ms::BlockAcquireResult::AbandonedUnswept(block) => {
323                    debug!("Acquire global block: AbandonedUnswep {:?}", block);
324                    block.store_tls(self.tls);
325                    block.sweep::<VM>();
326                    if block.has_free_cells() {
327                        self.add_to_available_blocks(bin, block, stress_test);
328                        return Some(block);
329                    } else {
330                        self.consumed_blocks[bin].push(block);
331                    }
332                }
333            }
334        }
335    }
336
337    fn init_block(&self, block: Block, cell_size: usize) {
338        debug_assert_ne!(cell_size, 0);
339        self.space.record_new_block(block);
340
341        // construct free list
342        let block_end = block.start() + Block::BYTES;
343        let mut old_cell = unsafe { Address::zero() };
344        let mut new_cell = block.start();
345
346        let final_cell = loop {
347            unsafe {
348                new_cell.store::<Address>(old_cell);
349            }
350            old_cell = new_cell;
351            new_cell += cell_size;
352            if new_cell + cell_size > block_end {
353                break old_cell;
354            };
355        };
356
357        block.store_free_list(final_cell);
358        block.store_block_cell_size(cell_size);
359        #[cfg(feature = "malloc_native_mimalloc")]
360        {
361            block.store_local_free_list(Address::ZERO);
362            block.store_thread_free_list(Address::ZERO);
363        }
364
365        self.store_block_tls(block);
366    }
367
368    #[cfg(feature = "malloc_native_mimalloc")]
369    fn free(&self, addr: Address) {
370        assert!(!addr.is_zero(), "Attempted to free zero address.");
371
372        use crate::util::ObjectReference;
373        let block = Block::from_unaligned_address(addr);
374        let block_tls = block.load_tls();
375
376        if self.tls == block_tls {
377            // same thread that allocated
378            let local_free = block.load_local_free_list();
379            unsafe {
380                addr.store(local_free);
381            }
382            block.store_local_free_list(addr);
383        } else {
384            // different thread to allocator
385            unreachable!(
386                "tlss don't match freeing from block {}, my tls = {:?}, block tls = {:?}",
387                block.start(),
388                self.tls,
389                block.load_tls()
390            );
391
392            // I am not sure whether the following code would be used to free a block for other thread. I will just keep it here as commented out.
393            // let mut success = false;
394            // while !success {
395            //     let thread_free = FreeListAllocator::<VM>::load_thread_free_list(block);
396            //     unsafe {
397            //         addr.store(thread_free);
398            //     }
399            //     success = FreeListAllocator::<VM>::cas_thread_free_list(&self, block, thread_free, addr);
400            // }
401        }
402
403        // unset allocation bit
404        // Note: We cannot use `unset_vo_bit_unsafe` because two threads may attempt to free
405        // objects at adjacent addresses, and they may share the same byte in the VO bit metadata.
406        crate::util::metadata::vo_bit::unset_vo_bit(unsafe {
407            ObjectReference::from_raw_address_unchecked(addr)
408        })
409    }
410
411    fn store_block_tls(&self, block: Block) {
412        block.store_tls(self.tls);
413    }
414
415    pub(crate) fn prepare(&mut self) {}
416
417    pub(crate) fn release(&mut self) {
418        for bin in 0..MI_BIN_FULL {
419            let unswept = self.unswept_blocks.get_mut(bin).unwrap();
420
421            // If we do eager sweeping, we should have no unswept blocks.
422            #[cfg(feature = "eager_sweeping")]
423            debug_assert!(unswept.is_empty());
424
425            let mut sweep_later = |list: &mut BlockList| {
426                list.release_blocks(self.space);
427
428                // For eager sweeping, that's it.  We just release unmarked blocks, and leave marked
429                // blocks to be swept later in the `SweepChunk` work packet.
430
431                // For lazy sweeping, we move blocks from available and consumed to unswept.  When
432                // an allocator tries to use them, they will sweep the block.
433                if cfg!(not(feature = "eager_sweeping")) {
434                    unswept.append(list);
435                }
436            };
437
438            sweep_later(&mut self.available_blocks[bin]);
439            sweep_later(&mut self.available_blocks_stress[bin]);
440            sweep_later(&mut self.consumed_blocks[bin]);
441        }
442
443        // We abandon block lists immediately.  Otherwise, some mutators will hold lots of blocks
444        // locally and prevent other mutators to use.
445        {
446            let mut global = self.space.get_abandoned_block_lists_in_gc().lock().unwrap();
447            self.abandon_blocks(&mut global);
448        }
449
450        self.space.release_packet_done();
451    }
452
453    fn abandon_blocks(&mut self, global: &mut AbandonedBlockLists) {
454        for i in 0..MI_BIN_FULL {
455            let available = self.available_blocks.get_mut(i).unwrap();
456            if !available.is_empty() {
457                global.available[i].append(available);
458            }
459
460            let available_stress = self.available_blocks_stress.get_mut(i).unwrap();
461            if !available_stress.is_empty() {
462                global.available[i].append(available_stress);
463            }
464
465            let consumed = self.consumed_blocks.get_mut(i).unwrap();
466            if !consumed.is_empty() {
467                global.consumed[i].append(consumed);
468            }
469
470            let unswept = self.unswept_blocks.get_mut(i).unwrap();
471            if !unswept.is_empty() {
472                global.unswept[i].append(unswept);
473            }
474        }
475    }
476}