Description
The following methods are labelled as unsafe
:
impl<VM: VMBinding> Mutator<VM> {
pub unsafe fn allocator(&self, selector: AllocatorSelector) -> &dyn Allocator<VM> {...}
pub unsafe fn allocator_mut(&mut self, selector: AllocatorSelector) -> &mut dyn Allocator<VM> {...}
pub unsafe fn allocator_impl<T: Allocator<VM>>(&self, selector: AllocatorSelector) -> &T {...}
pub unsafe fn allocator_impl_mut<T: Allocator<VM>>(&mut self, selector: AllocatorSelector) -> &mut T {...}
}
impl<VM: VMBinding> Allocators<VM> {
pub unsafe fn get_allocator(&self, selector: AllocatorSelector) -> &dyn Allocator<VM> {...}
pub unsafe fn get_typed_allocator<T: Allocator<VM>>(&self, selector: AllocatorSelector) -> &T {...}
pub unsafe fn get_allocator_mut(&mut self, selector: AllocatorSelector) -> &mut dyn Allocator<VM> {...}
pub unsafe fn get_typed_allocator_mut<T: Allocator<VM>>(&mut self, selector: AllocatorSelector) -> &mut T {...}
}
And those functions are called in many places. (Actually only Allocators::get_allocator_mut
is properly used. The rest are only used in wrappers or tests.)
- mutator prepare/release functions (6 call sites), such as
immix_mutator_release
. It gets the appropriate allocators and callrelease
orreset
functions. alloc
andpost_alloc
functions (5 call sites) ofMutator
. Users callalloc
andpost_alloc
withAllocatorSemantics
. The first thing those alloc functions do is looking up the proper allocator (as&dyn Allocator
), and then it callsAllocator::alloc
(with dynamic dispatch).Mutator::on_destroy
. It iterates through all allocators and callon_mutator_destroy
.
Why are they unsafe?
Those functions all serve the purpose of getting an allocator from a given AllocatorSelector
. They can't guarantee to return an allocator because
- An
AllocatorSelector
may not always be valid. It may select an allocator that does not exist for the current Plan. - The
Allocators
struct holds allocators in[MaybeUninit<T>; MAX_T_ALLOCATOR]
whereT
is the allocator type. The raw array does not record the number of elements "pushed" into the array, andMaybeUninit
has no way to check if it is initialized at run time.
If #1318 is fixed, the Allocators
struct will be able to hold the concrete Allocator
instances in vectors. Then we will be able to use Vec::get
to return an Option<&allocator>
which gracefully handles invalid AllocatorSelector
and solves both problems above.
Impacts on allocation site.
Mutator::alloc
and related functions currently contain the following call site:
fn alloc(&mut self, size: usize, align: usize, offset: usize, semantics: AllocationSemantics) -> Address {
let allocator = unsafe {
self.allocators
.get_allocator_mut(self.config.allocator_mapping[semantics])
};
allocator.alloc(size, align, offset)
}
After the change, get_allocator_mut
will return an Option<&dyn Allocator>
. It will become
let allocator = self.allocators.get_allocator_mut(self.config.allocator_mapping[semantics]).unwrap();
This involves an extra unwrap()
operation. But I assume the cost of this is trivial compared to the allocator_mapping
look-up and the dynamic dispatching. Any VM binding that cares about performance should have inlined the allocation fast path, and used the BumpPointer
directly instead of relying on those dynamism. In my opinion, it is not worth trade safety for such trivial performance gain in uninlined code.
Regarding mutator prepare/release functions
It could be possible to simply iterate through all Allocators using for loops (which becomes possible because we are now using Vec
with no uninitialized elements) and call their prepare/release/end_of_gc methods. (Note that currently Mutator::on_destroy
is doing this indirectly via Mutators::get_all_allocator_selectors()
.) This should be enough to handle most plans. And this should make the common_release_func
introduced in #1308 easier to implement.
One exception is SemiSpace which needs to rebind the BumpAllocator
with a different space. This can be handled specially.