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