mmtk/util/alloc/
immix_allocator.rs

1use std::sync::atomic::Ordering;
2use std::sync::Arc;
3
4use super::allocator::{align_allocation_no_fill, fill_alignment_gap, AllocatorContext};
5use super::BumpPointer;
6use crate::policy::immix::block::Block;
7use crate::policy::immix::line::*;
8use crate::policy::immix::ImmixSpace;
9use crate::policy::space::Space;
10use crate::util::alloc::allocator::get_maximum_aligned_size;
11use crate::util::alloc::Allocator;
12use crate::util::linear_scan::Region;
13use crate::util::opaque_pointer::VMThread;
14use crate::util::rust_util::unlikely;
15use crate::util::Address;
16use crate::vm::*;
17
18/// Immix allocator
19#[repr(C)]
20pub struct ImmixAllocator<VM: VMBinding> {
21    /// [`VMThread`] associated with this allocator instance
22    pub tls: VMThread,
23    /// The fastpath bump pointer.
24    pub bump_pointer: BumpPointer,
25    /// [`Space`](src/policy/space/Space) instance associated with this allocator instance.
26    space: &'static ImmixSpace<VM>,
27    context: Arc<AllocatorContext<VM>>,
28    /// *unused*
29    hot: bool,
30    /// Is this a copy allocator?
31    copy: bool,
32    /// Bump pointer for large objects
33    pub(in crate::util::alloc) large_bump_pointer: BumpPointer,
34    /// Is the current request for large or small?
35    request_for_large: bool,
36    /// Hole-searching cursor
37    line: Option<Line>,
38}
39
40impl<VM: VMBinding> ImmixAllocator<VM> {
41    pub(crate) fn reset(&mut self) {
42        self.bump_pointer.reset(Address::ZERO, Address::ZERO);
43        self.large_bump_pointer.reset(Address::ZERO, Address::ZERO);
44        self.request_for_large = false;
45        self.line = None;
46    }
47}
48
49impl<VM: VMBinding> Allocator<VM> for ImmixAllocator<VM> {
50    fn get_space(&self) -> &'static dyn Space<VM> {
51        self.space as _
52    }
53
54    fn get_context(&self) -> &AllocatorContext<VM> {
55        &self.context
56    }
57
58    fn does_thread_local_allocation(&self) -> bool {
59        true
60    }
61
62    fn get_thread_local_buffer_granularity(&self) -> usize {
63        crate::policy::immix::block::Block::BYTES
64    }
65
66    fn alloc(&mut self, size: usize, align: usize, offset: usize) -> Address {
67        debug_assert!(
68            size <= crate::policy::immix::MAX_IMMIX_OBJECT_SIZE,
69            "Trying to allocate a {} bytes object, which is larger than MAX_IMMIX_OBJECT_SIZE {}",
70            size,
71            crate::policy::immix::MAX_IMMIX_OBJECT_SIZE
72        );
73
74        let result = align_allocation_no_fill::<VM>(self.bump_pointer.cursor, align, offset);
75        let new_cursor = result + size;
76
77        if new_cursor > self.bump_pointer.limit {
78            trace!(
79                "{:?}: Thread local buffer used up, go to alloc slow path",
80                self.tls
81            );
82            if get_maximum_aligned_size::<VM>(size, align) > Line::BYTES {
83                // Size larger than a line: do large allocation
84                self.overflow_alloc(size, align, offset)
85            } else {
86                // Size smaller than a line: fit into holes
87                self.alloc_slow_hot(size, align, offset)
88            }
89        } else {
90            // Simple bump allocation.
91            fill_alignment_gap::<VM>(self.bump_pointer.cursor, result);
92            self.bump_pointer.cursor = new_cursor;
93            trace!(
94                "{:?}: Bump allocation size: {}, result: {}, new_cursor: {}, limit: {}",
95                self.tls,
96                size,
97                result,
98                self.bump_pointer.cursor,
99                self.bump_pointer.limit
100            );
101            result
102        }
103    }
104
105    /// Acquire a clean block from ImmixSpace for allocation.
106    fn alloc_slow_once(&mut self, size: usize, align: usize, offset: usize) -> Address {
107        trace!("{:?}: alloc_slow_once", self.tls);
108        self.acquire_clean_block(size, align, offset)
109    }
110
111    /// This is called when precise stress is used. We try use the thread local buffer for
112    /// the allocation (after restoring the correct limit for thread local buffer). If we cannot
113    /// allocate from thread local buffer, we will go to the actual slowpath. After allocation,
114    /// we will set the fake limit so future allocations will fail the slowpath and get here as well.
115    fn alloc_slow_once_precise_stress(
116        &mut self,
117        size: usize,
118        align: usize,
119        offset: usize,
120        need_poll: bool,
121    ) -> Address {
122        trace!("{:?}: alloc_slow_once_precise_stress", self.tls);
123        // If we are required to make a poll, we call acquire_clean_block() which will acquire memory
124        // from the space which includes a GC poll.
125        if need_poll {
126            trace!(
127                "{:?}: alloc_slow_once_precise_stress going to poll",
128                self.tls
129            );
130            let ret = self.acquire_clean_block(size, align, offset);
131            // Set fake limits so later allocation will fail in the fastpath, and end up going to this
132            // special slowpath.
133            self.set_limit_for_stress();
134            trace!(
135                "{:?}: alloc_slow_once_precise_stress done - forced stress poll",
136                self.tls
137            );
138            return ret;
139        }
140
141        // We are not yet required to do a stress GC. We will try to allocate from thread local
142        // buffer if possible.  Restore the fake limit to the normal limit so we can do thread
143        // local allocation normally. Check if we have exhausted our current thread local block,
144        // and if so, then directly acquire a new one
145        self.restore_limit_for_stress();
146        let ret = if self.require_new_block(size, align, offset) {
147            // We don't have enough space in thread local block to service the allocation request,
148            // hence allocate a new block
149            trace!(
150                "{:?}: alloc_slow_once_precise_stress - acquire new block",
151                self.tls
152            );
153            self.acquire_clean_block(size, align, offset)
154        } else {
155            // This `alloc()` call should always succeed given the if-branch checks if we are out
156            // of thread local block space
157            trace!("{:?}: alloc_slow_once_precise_stress - alloc()", self.tls,);
158            self.alloc(size, align, offset)
159        };
160        // Set fake limits
161        self.set_limit_for_stress();
162        ret
163    }
164
165    fn get_tls(&self) -> VMThread {
166        self.tls
167    }
168}
169
170impl<VM: VMBinding> ImmixAllocator<VM> {
171    pub(crate) fn new(
172        tls: VMThread,
173        space: Option<&'static dyn Space<VM>>,
174        context: Arc<AllocatorContext<VM>>,
175        copy: bool,
176    ) -> Self {
177        ImmixAllocator {
178            tls,
179            space: space.unwrap().downcast_ref::<ImmixSpace<VM>>().unwrap(),
180            context,
181            bump_pointer: BumpPointer::default(),
182            hot: false,
183            copy,
184            large_bump_pointer: BumpPointer::default(),
185            request_for_large: false,
186            line: None,
187        }
188    }
189
190    pub(crate) fn immix_space(&self) -> &'static ImmixSpace<VM> {
191        self.space
192    }
193
194    /// Large-object (larger than a line) bump allocation.
195    fn overflow_alloc(&mut self, size: usize, align: usize, offset: usize) -> Address {
196        trace!("{:?}: overflow_alloc", self.tls);
197        let start = align_allocation_no_fill::<VM>(self.large_bump_pointer.cursor, align, offset);
198        let end = start + size;
199        if end > self.large_bump_pointer.limit {
200            self.request_for_large = true;
201            let rtn = self.alloc_slow_inline(size, align, offset);
202            self.request_for_large = false;
203            rtn
204        } else {
205            fill_alignment_gap::<VM>(self.large_bump_pointer.cursor, start);
206            self.large_bump_pointer.cursor = end;
207            start
208        }
209    }
210
211    /// Bump allocate small objects into recyclable lines (i.e. holes).
212    fn alloc_slow_hot(&mut self, size: usize, align: usize, offset: usize) -> Address {
213        trace!("{:?}: alloc_slow_hot", self.tls);
214        if self.acquire_recyclable_lines(size, align, offset) {
215            // If stress test is active, then we need to go to the slow path instead of directly
216            // calling `alloc()`. This is because the `acquire_recyclable_lines()` function
217            // manipulates the cursor and limit if a line can be recycled and if we directly call
218            // `alloc()` after recyling a line, then we will miss updating the `allocation_bytes`
219            // as the newly recycled line will service the allocation request. If we set the stress
220            // factor limit directly in `acquire_recyclable_lines()`, then we risk running into an
221            // loop of failing the fastpath (i.e. `alloc()`) and then trying to allocate from a
222            // recyclable line.  Hence, we bring the "if we're in stress test" check up a level and
223            // directly call `alloc_slow_inline()` which will properly account for the allocation
224            // request as well as allocate from the newly recycled line
225            let stress_test = self.context.options.is_stress_test_gc_enabled();
226            let precise_stress = *self.context.options.precise_stress;
227            if unlikely(stress_test && precise_stress) {
228                self.alloc_slow_inline(size, align, offset)
229            } else {
230                self.alloc(size, align, offset)
231            }
232        } else {
233            self.alloc_slow_inline(size, align, offset)
234        }
235    }
236
237    /// Search for recyclable lines.
238    fn acquire_recyclable_lines(&mut self, size: usize, align: usize, offset: usize) -> bool {
239        while self.line.is_some() || self.acquire_recyclable_block() {
240            let line = self.line.unwrap();
241            if let Some((start_line, end_line)) =
242                self.immix_space().get_next_available_lines(self.copy, line)
243            {
244                // Find recyclable lines. Update the bump allocation cursor and limit.
245                self.bump_pointer.cursor = start_line.start();
246                self.bump_pointer.limit = end_line.start();
247                trace!(
248                    "{:?}: acquire_recyclable_lines -> {:?} [{:?}, {:?}) {:?}",
249                    self.tls,
250                    self.line,
251                    start_line,
252                    end_line,
253                    self.tls
254                );
255                crate::util::memory::zero(
256                    self.bump_pointer.cursor,
257                    self.bump_pointer.limit - self.bump_pointer.cursor,
258                );
259                debug_assert!(
260                    align_allocation_no_fill::<VM>(self.bump_pointer.cursor, align, offset) + size
261                        <= self.bump_pointer.limit
262                );
263                let block = line.block();
264                self.line = if end_line == block.end_line() {
265                    // Hole searching reached the end of a reusable block. Set the hole-searching cursor to None.
266                    None
267                } else {
268                    // Update the hole-searching cursor to None.
269                    Some(end_line)
270                };
271                // mark objects if concurrent marking is active
272                if self.immix_space().should_allocate_as_live() {
273                    let state = self.space.line_mark_state.load(Ordering::Acquire);
274                    Line::eager_mark_lines::<VM>(state, start_line..end_line);
275                    // Objects allocated here are not in the SATB snapshot, log them.
276                    if self.immix_space().common().needs_log_bit {
277                        VM::VMObjectModel::GLOBAL_LOG_BIT_SPEC.bulk_mark_as_logged(
278                            start_line.start(),
279                            end_line.start() - start_line.start(),
280                        );
281                    }
282                }
283                return true;
284            } else {
285                // No more recyclable lines. Set the hole-searching cursor to None.
286                self.line = None;
287            }
288        }
289        false
290    }
291
292    /// Get a recyclable block from ImmixSpace.
293    fn acquire_recyclable_block(&mut self) -> bool {
294        match self.immix_space().get_reusable_block(self.copy) {
295            Some(block) => {
296                trace!("{:?}: acquire_recyclable_block -> {:?}", self.tls, block);
297                // Set the hole-searching cursor to the start of this block.
298                self.line = Some(block.start_line());
299                true
300            }
301            _ => false,
302        }
303    }
304
305    // Get a clean block from ImmixSpace.
306    fn acquire_clean_block(&mut self, size: usize, align: usize, offset: usize) -> Address {
307        match self.immix_space().get_clean_block(
308            self.tls,
309            self.copy,
310            self.get_context().get_alloc_options(),
311        ) {
312            None => Address::ZERO,
313            Some(block) => {
314                trace!(
315                    "{:?}: Acquired a new block {:?} -> {:?}",
316                    self.tls,
317                    block.start(),
318                    block.end()
319                );
320                // FIXME: Why don't we need this for LXR? Conix needs this.
321                if !self.immix_space().rc_enabled {
322                    // Bulk clear stale line mark state
323                    Line::MARK_TABLE
324                        .bzero_metadata(block.start(), crate::policy::immix::block::Block::BYTES);
325                    // mark objects if concurrent marking is active
326                    if self.immix_space().should_allocate_as_live() {
327                        let state = self.space.line_mark_state.load(Ordering::Acquire);
328                        Line::eager_mark_lines::<VM>(state, block.start_line()..block.end_line());
329                        // Objects allocated here are not in the SATB snapshot, log them
330                        if self.immix_space().common().needs_log_bit {
331                            VM::VMObjectModel::GLOBAL_LOG_BIT_SPEC
332                                .bulk_mark_as_logged(block.start(), Block::BYTES);
333                        }
334                    }
335                }
336                if self.request_for_large {
337                    self.large_bump_pointer.cursor = block.start();
338                    self.large_bump_pointer.limit = block.end();
339                } else {
340                    self.bump_pointer.cursor = block.start();
341                    self.bump_pointer.limit = block.end();
342                }
343                self.alloc(size, align, offset)
344            }
345        }
346    }
347
348    /// Return whether the TLAB has been exhausted and we need to acquire a new block. Assumes that
349    /// the buffer limits have been restored using [`ImmixAllocator::restore_limit_for_stress`].
350    /// Note that this function may implicitly change the limits of the allocator.
351    fn require_new_block(&mut self, size: usize, align: usize, offset: usize) -> bool {
352        let result = align_allocation_no_fill::<VM>(self.bump_pointer.cursor, align, offset);
353        let new_cursor = result + size;
354        let insufficient_space = new_cursor > self.bump_pointer.limit;
355
356        // We want this function to behave as if `alloc()` has been called. Hence, we perform a
357        // size check and then return the conditions where `alloc_slow_inline()` would be called
358        // in an `alloc()` call, namely when both `overflow_alloc()` and `alloc_slow_hot()` fail
359        // to service the allocation request
360        if insufficient_space && get_maximum_aligned_size::<VM>(size, align) > Line::BYTES {
361            let start =
362                align_allocation_no_fill::<VM>(self.large_bump_pointer.cursor, align, offset);
363            let end = start + size;
364            end > self.large_bump_pointer.limit
365        } else {
366            // We try to acquire recyclable lines here just like `alloc_slow_hot()`
367            insufficient_space && !self.acquire_recyclable_lines(size, align, offset)
368        }
369    }
370
371    /// Set fake limits for the bump allocation for stress tests. The fake limit is the remaining
372    /// thread local buffer size, which should be always smaller than the bump cursor. This method
373    /// may be reentrant. We need to check before setting the values.
374    fn set_limit_for_stress(&mut self) {
375        if self.bump_pointer.cursor < self.bump_pointer.limit {
376            let old_limit = self.bump_pointer.limit;
377            let new_limit =
378                unsafe { Address::from_usize(self.bump_pointer.limit - self.bump_pointer.cursor) };
379            self.bump_pointer.limit = new_limit;
380            trace!(
381                "{:?}: set_limit_for_stress. normal c {} l {} -> {}",
382                self.tls,
383                self.bump_pointer.cursor,
384                old_limit,
385                new_limit,
386            );
387        }
388
389        if self.large_bump_pointer.cursor < self.large_bump_pointer.limit {
390            let old_lg_limit = self.large_bump_pointer.limit;
391            let new_lg_limit = unsafe {
392                Address::from_usize(self.large_bump_pointer.limit - self.large_bump_pointer.cursor)
393            };
394            self.large_bump_pointer.limit = new_lg_limit;
395            trace!(
396                "{:?}: set_limit_for_stress. large c {} l {} -> {}",
397                self.tls,
398                self.large_bump_pointer.cursor,
399                old_lg_limit,
400                new_lg_limit,
401            );
402        }
403    }
404
405    /// Restore the real limits for the bump allocation so we can properly do a thread local
406    /// allocation. The fake limit is the remaining thread local buffer size, and we restore the
407    /// actual limit from the size and the cursor. This method may be reentrant. We need to check
408    /// before setting the values.
409    fn restore_limit_for_stress(&mut self) {
410        if self.bump_pointer.limit < self.bump_pointer.cursor {
411            let old_limit = self.bump_pointer.limit;
412            let new_limit = self.bump_pointer.cursor + self.bump_pointer.limit.as_usize();
413            self.bump_pointer.limit = new_limit;
414            trace!(
415                "{:?}: restore_limit_for_stress. normal c {} l {} -> {}",
416                self.tls,
417                self.bump_pointer.cursor,
418                old_limit,
419                new_limit,
420            );
421        }
422
423        if self.large_bump_pointer.limit < self.large_bump_pointer.cursor {
424            let old_lg_limit = self.large_bump_pointer.limit;
425            let new_lg_limit =
426                self.large_bump_pointer.cursor + self.large_bump_pointer.limit.as_usize();
427            self.large_bump_pointer.limit = new_lg_limit;
428            trace!(
429                "{:?}: restore_limit_for_stress. large c {} l {} -> {}",
430                self.tls,
431                self.large_bump_pointer.cursor,
432                old_lg_limit,
433                new_lg_limit,
434            );
435        }
436    }
437}