std::alloc::Global
apparently doesn't even implement GlobalAlloc
, so you're correct on the surface — Rust's std collections use Global as Allocator
, not GlobalAlloc
. However, these implementations are merely wrappers around the stable free functions (e.g. alloc
and friends) so it goes through the GlobalAlloc
interface either way, and System
also implements Allocator
in terms of GlobalAlloc
.
And I think the “grow a lot then shrink to roughly final size” case using a non-Vec
type, as well as the giant page-sized vector, is fully reasonable. It's already the case that any functionality that doesn't change the length works on [T]
instead of Vec<T>
, so the full extent of API assuming Vec
is mostly limited to that utilizing a buffer of some sort. (And I think we should encourage using Box<[T]>
more when the length is fixed once the type is constructed.)
Vec
is a “simple enough” and “good enough” option that won't be non-linearly horrible (per Vec
) in 99.9% of applications. A SmallVec
that uses inline storage for “small” vectors and a HugeVec
that uses page allocation tricks on platforms that can do so would be nice to haves for sure, but I think the costs of only having Vec
in std are a bit overstated.
Everything would be possible. Growth strategy isn't covered, but I think it's relevant to point out that the Storage proposal (alternative to directly using the allocation trait in collections) handles the SmallVec
and HugeVec
cases. The short-ish version (laden with contentious choices):
struct Vec<T, S: Store = SingleStore<Global>> {
ptr: S::Handle,
len: usize,
cap: usize,
store: S,
}
struct SingleStore<A> {
data: ptr::NonNull<u8>, // !
alloc: A,
}
unsafe impl Store for SingleStore<impl Allocator> {
type Handle = (), // !
fn allocate(&mut self, layout: Layout) -> Result<Self::Handle, AllocError> = try {
let ptr = self.alloc.allocate(layout)?;
self.data = ptr::NonNull::new(ptr)
.unwrap_or_else(|| handle_alloc_error(layout));
()
}
unsafe fn deallocate(&mut self, (): Self::Handle, layout: Layout) {
unsafe { self.alloc.deallocate(self.data, layout) }
}
/* … grow/shrink etc … */
unsafe fn resolve(&self, (): Self::Handle, layout: Layout) -> ptr::NonNull<u8> {
self.data
}
}
struct SmallStore<T, A> {
union {
outline: ptr::NonNull<u8>,
inline: UnsafeCell<MaybeUninit<T>>,
},
alloc: A,
}
unsafe impl<T> Store for SmallStore<T, impl Allocator> {
type Handle = (), // !
fn allocate(&mut self, layout: Layout) -> Result<Self::Handle, AllocError> = try {
if layout.fits_in(Layout::new::<T>()) {
// nop
} else {
let ptr = self.alloc.allocate(layout)?;
self.data = ptr::NonNull::new(ptr)
.unwrap_or_else(|| handle_alloc_error(layout));
()
}
}
unsafe fn deallocate(&mut self, (): Self::Handle, layout: Layout) {
if layout.fits_in(Layout::new::<T>()) {
// nop
} else {
unsafe { self.alloc.deallocate(self.data, layout) }
}
}
unsafe fn resolve(&self, (): Self::Handle, layout: Layout) -> ptr::NonNull<u8> {
if layout.fits_in(Layout::new::<T>()) {
unsafe { self.inline.as_ptr().cast() }
} else {
unsafe { self.outline }
}
}
/* … and so on … */
}
/* … whatever PageVec would need to do … */
Note to self: Vec::as_ptr
's specification requires its storage to be vec-shared-mutable, removing that axis of behavior from the Store
trait hierarchy. However, the mut split for de/alloc is still an annoying source of seemingly incidental complexity, and ArrayVec
not optimizing out capacity still leaves us needing to duplicate Vec
's API, thus making Store
even more difficult to justify the intrinsic complexity of.
I may re-champion storage if we can find a way to optimize length/capacity without bloating the API to a painfully inelegant level again. But even then, the lock to non-meta-laid-out allocations is also quite an annoying limitation… but one necessary for optimal SmallStore
…
This isn't for discussion in this thread, though. Just musing on why this is so difficult to get right.
Very much agree. Maybe it should be reserve_shrink
or similar, but there definitely should be a way to ask for heuristic driven shrinking that works well with the growth heuristics.