Description
The current push to associate an allocator to boxes (among other types) is targeting something like Box<T, A: Alloc>
. This limits the design space of custom allocators, though, because it forces one of two approaches for accessing the Alloc
:
- Use a single static global state (or pointer or reference to the state).
- Embed a pointer or reference to the state inside every box.
There are many kinds of allocators that these limitations rule out. For example, perhaps you want to have multiple independent "Arena" or "Region" allocators that each allocate from a dedicated slab of memory. Since you want to have multiple independent allocators, you cannot (efficiently) use approach 1. So you're forced to use approach 2, which is extremely inefficient, requiring a whole extra pointer to be stored in each Box
.
One approach for avoiding this limitation would be something like the following:
pub struct Box<T: ?Sized, A: BuildAlloc>(Unique<T>, A);
pub trait BuildAlloc {
type Alloc: Alloc;
/// Build an `Alloc` given a `ptr` pointer to memory with the given
/// `layout` that was allocated by the associated allocator.
///
/// # Safety
///
/// This function is unsafe because undefined behavior can result
/// if the caller does not ensure all of the following:
///
/// * `ptr` must denote a block of memory currently allocated via
/// this allocator,
///
/// * `layout` must *fit* that block of memory,
///
/// * In addition to fitting the block of memory `layout`, the
/// alignment of the `layout` must match the alignment used
/// to allocate that block of memory.
unsafe fn build_alloc(&mut self, ptr: NonNull<u8>, layout: Layout) -> Self::Alloc;
}
Note that BuildAlloc
provides the method build_alloc
, which accepts &mut self
and a pointer and layout for memory that was allocated by the associated allocator, and returns an Alloc
implementation. This gives many options for locating the allocator's state and generating an Alloc
. Approach 1 (global state) is of course still possible, and so is approach 2 (embed a pointer in every box). But this also makes approach 3 possible:
- Calculate a pointer to the allocator state from a pointer to heap memory allocated by that allocator.
This makes many more clever and efficient kinds of allocators possible, such as an allocator that allocates all 8-byte allocations from aligned 1 MiB blocks, and stashes a pointer to its own state at the beginning of each of those 1 MiB blocks. Such an allocator can implement BuildAlloc
to check the size of the allocated memory and if it is 8-bytes then mask off the lowest 10 bits from ptr
to get a (1 MiB aligned) pointer to the allocator's state, from which it can construct an Alloc
.
To clarify, BuildAlloc
makes it possible to calculate a pointer to the allocator's state from the Box
itself:
impl<T: ?Sized, A: BuildAlloc> Box<T, A> {
fn get_alloc(&self) -> A::Alloc {
self.1.build_alloc(
NonNull::from(self.0).cast::<u8>(),
Layout::for_value(self.as_ref())
)
}
}
Since the Box
provides a pointer to memory allocated by the allocator, the allocator can use its control of the pointers it returns and the memory around those pointers to efficiently locate its own state and generate an Alloc
. This makes many more kinds of clever and efficient allocators possible.
Relevant comments from PR #58457: Associate an allocator to boxes:
- Associate an allocator to boxes rust#58457 (comment)
- Associate an allocator to boxes rust#58457 (comment)
- Associate an allocator to boxes rust#58457 (comment)
- Associate an allocator to boxes rust#58457 (comment)
- this comment has the most complete usage example, although it defines
AllocFrom
(old name ofBuildAlloc
) to have a method for each container type, when really we just need a single method that accepts a pointer to heap memory
- this comment has the most complete usage example, although it defines
Edited 2019-05-04: Rename AllocFrom
to BuildAlloc
for consistency with BuildHasher
.
Edited 2019-05-07: Adjust build_alloc
signature to match dealloc
.