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.