diff --git a/src/errors.rs b/src/errors.rs index b470e07d..74ff1d60 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -12,7 +12,7 @@ use { } }; -#[derive(Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum SmallVecError { CapacityOverflow, AllocationError(Layout) diff --git a/src/lib.rs b/src/lib.rs index 9f391826..df2359fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -265,34 +265,98 @@ impl SmallVec { } } - /// Creates a `SmallVec` directly from the raw components of another - /// `SmallVec`. + /// Creates a `SmallVec` directly from a `NonNull` pointer, a length, + /// and a capacity. /// /// # Safety /// /// This is highly unsafe, due to the number of invariants that aren’t /// checked: /// - /// - `ptr` needs to have been previously allocated via `SmallVec` from its - /// spilled storage (at least, it’s highly likely to be incorrect if it - /// wasn’t). - /// - `ptr`’s `T` type needs to be the same size and alignment that it was - /// allocated with + /// - `ptr` must be [*currently allocated*] via the global allocator. + /// As such, if it belonged to a `SmallVec`, it must have been spilled to + /// the heap. + /// - `T` needs to have the same alignment as what `ptr` was allocated with. + /// (`T` having a less strict alignment is not sufficient, the alignment + /// really needs to be equal to satisfy the [`dealloc`] requirement that + /// memory must be allocated and deallocated with the same layout.) + /// - The size of `T` times the `capacity` (i.e. the allocated size in + /// bytes) needs to be the same size as the pointer was allocated with. + /// (Because similar to alignment, [`dealloc`] must be called with the + /// same layout `size`.) /// - `length` needs to be less than or equal to `capacity`. + /// - The first `length` values must be properly initialized values of type + /// `T`. /// - `capacity` needs to be the capacity that the pointer was allocated /// with. + /// - The allocated size in bytes must be no larger than `isize::MAX`. See + /// the safety documentation of [`std::pointer::offset`]. + /// + /// These requirements are always upheld by any `ptr` that has been + /// allocated via any `SmallVec`. Other allocation sources are + /// allowed if the invariants are upheld. + /// + /// Additionally, `capacity` must be greater than `N`; that is, the new + /// `SmallVec` must need to spill over into heap allocated storage. This + /// condition is asserted against. /// /// Violating these may cause problems like corrupting the allocator’s /// internal data structures. /// + /// The ownership of `ptr` is effectively transferred to the `SmallVec` which may then deallocate, reallocate or change the contents of + /// memory pointed to by the pointer at will. Ensure that nothing else + /// uses the pointer after calling this function. + /// + /// [*currently allocated*]: std::alloc::Allocator#currently-allocated-memory + /// + /// # Examples + /// + /// Creates a `SmallVec` directly from a `NonNull` pointer, a + /// length, a capacity, and an allocator. + /// + /// # Safety + /// + /// This is highly unsafe, due to the number of invariants that aren’t + /// checked: + /// + /// - `ptr` must be [*currently allocated*] via the given allocator `alloc`. + /// If it belonged to a `SmallVec`, it must have been spilled to + /// the heap. + /// - `T` needs to have the same alignment as what `ptr` was allocated with. + /// (`T` having a less strict alignment is not sufficient, the alignment + /// really needs to be equal to satisfy the [`dealloc`] requirement that + /// memory must be allocated and deallocated with the same layout.) + /// - The size of `T` times the `capacity` (i.e. the allocated size in + /// bytes) needs to be the same size as the pointer was allocated with. + /// (Because similar to alignment, [`dealloc`] must be called with the + /// same layout `size`.) + /// - `length` needs to be less than or equal to `capacity`. + /// - The first `length` values must be properly initialized values of type + /// `T`. + /// - `capacity` needs to [*fit*] the layout size that the pointer was + /// allocated with. + /// - The allocated size in bytes must be no larger than `isize::MAX`. See + /// the safety documentation of [`std::pointer::offset`]. + /// + /// These requirements are always upheld by any `ptr` that has been + /// allocated via any `SmallVec`. Other allocation sources are + /// allowed if the invariants are upheld. + /// /// Additionally, `capacity` must be greater than `N`; that is, the new /// `SmallVec` must need to spill over into heap allocated storage. This /// condition is asserted against. /// - /// The ownership of `ptr` is effectively transferred to the `SmallVec` - /// which may then deallocate, reallocate or change the contents of memory - /// pointed to by the pointer at will. Ensure that nothing else uses the - /// pointer after calling this function. + /// Violating these may cause problems like corrupting the allocator’s + /// internal data structures. + /// + /// The ownership of `ptr` is effectively transferred to the `SmallVec` which may then deallocate, reallocate or change the contents of + /// memory pointed to by the pointer at will. Ensure that nothing else + /// uses the pointer after calling this function. + /// + /// [*currently allocated*]: std::alloc::Allocator#currently-allocated-memory + /// [*fit*]: std::alloc::Allocator#memory-fitting /// /// # Examples /// @@ -301,17 +365,11 @@ impl SmallVec { /// /// let mut v: SmallVec<_, 1> = SmallVec::from([1, 2, 3]); /// - /// // Pull out the important parts of `v`. - /// let p = v.as_mut_ptr(); - /// let length = v.len(); - /// let cap = v.capacity(); + /// // Pull out the important parts of `v` /// let spilled = v.spilled(); + /// let (ptr, length, capacity) = v.into_parts(); /// /// unsafe { - /// // Forget all about `v`. The heap allocation that stored the - /// // three values won't be deallocated. - /// std::mem::forget(v); - /// /// // Overwrite memory with [4, 5, 6]. /// // /// // This is only safe if `spilled` is true! Otherwise, we are @@ -319,35 +377,70 @@ impl SmallVec { /// // stack. /// assert!(spilled); /// for i in 0..length { - /// std::ptr::write(p.add(i), 4 + i); + /// ptr.add(i).write(4 + i); /// } /// /// // Put everything back together into a SmallVec with a different - /// // amount of inline storage, but which is still less than `cap`. - /// let rebuilt = SmallVec::<_, 2>::from_raw_parts(p, length, cap); - /// assert_eq!(&*rebuilt, &[4, 5, 6]); + /// // amount of inline storage, but which is still less than `capacity`. + /// let rebuilt = SmallVec::<_, 2>::from_parts(ptr, length, capacity); + /// assert_eq!(rebuilt, &[4, 5, 6]); /// } /// ``` #[inline] - pub unsafe fn from_raw_parts( - ptr: *mut T, + pub unsafe fn from_parts( + ptr: NonNull, length: usize, capacity: usize ) -> SmallVec { - assert!(!Self::IS_ZST); - - // SAFETY: We require caller to provide same ptr as we alloc - // and we never alloc null pointer. - let ptr = unsafe { - debug_assert!(!ptr.is_null(), "Called `from_raw_parts` with null pointer."); - NonNull::new_unchecked(ptr) - }; + unsafe { Self::from_parts_in(ptr, length, capacity, Global) } + } - SmallVec { - length: LocatedLength::new(length, true), - raw: RawSmallVec::new_heap(ptr, capacity), - allocator: Global - } + /// Decomposes a `SmallVec` into its raw components: + /// `(NonNull pointer, length, capacity)`. + /// + /// Returns the `NonNull` pointer to the underlying data, the length of + /// the vector (in elements), and the allocated capacity of the + /// data (in elements). These are the same arguments + /// in the same order as the arguments to [`from_parts`]. + /// + /// After calling this function, the caller is responsible for the + /// memory previously managed by the `SmallVec`. Most often, one does + /// this by converting the raw pointer, length, and capacity back + /// into a `SmallVec` with the [`from_parts`] function, allowing the + /// destructor to perform the cleanup; more generally, + /// if `T` is non-zero-sized and the capacity is non-zero, one may use + /// any method that calls [`dealloc`] with a layout of + /// `Layout::array::(capacity)`; if `T` is zero-sized or the + /// capacity is zero, nothing needs to be done. + /// + /// [`from_parts`]: SmallVec::from_parts + /// [`dealloc`]: alloc::alloc::GlobalAlloc::dealloc + /// + /// # Panics + /// + /// This function will panic if the contents are not spilled on the heap. + /// + /// # Examples + /// + /// ``` + /// # use smallvec::SmallVec; + /// let v: SmallVec = SmallVec::from([-1, 0, 1]); + /// + /// let (ptr, length, cap) = v.into_parts(); + /// + /// let rebuilt = unsafe { + /// // We can now make changes to the components, such as + /// // transmuting the raw pointer to a compatible type. + /// let ptr = ptr.cast::(); + /// + /// SmallVec::::from_parts(ptr, length, cap) + /// }; + /// assert_eq!(rebuilt, [4294967295, 0, 1]); + /// ``` + #[inline] + pub fn into_parts(self) -> (NonNull, usize, usize) { + let (ptr, len, capacity, _) = self.into_parts_with_alloc(); + (ptr, len, capacity) } } @@ -1203,25 +1296,115 @@ impl SmallVec { } } - /// Decomposes a `SmallVec` into its raw components: - /// `(pointer, length, capacity)`. + /// Creates a `SmallVec` directly from a `NonNull` pointer, a + /// length, a capacity, and an allocator. /// - /// Returns the raw pointer to the underlying data, the length of - /// the vector (in elements), and the allocated capacity of the - /// data (in elements). These are the same arguments in the same - /// order as the arguments to [`from_raw_parts`]. + /// # Safety + /// + /// This is highly unsafe, due to the number of invariants that aren’t + /// checked: + /// + /// - `ptr` must be [*currently allocated*] via the given allocator `alloc`. + /// If it belonged to a `SmallVec`, it must have been spilled to + /// the heap. + /// - `T` needs to have the same alignment as what `ptr` was allocated with. + /// (`T` having a less strict alignment is not sufficient, the alignment + /// really needs to be equal to satisfy the [`dealloc`] requirement that + /// memory must be allocated and deallocated with the same layout.) + /// - The size of `T` times the `capacity` (i.e. the allocated size in + /// bytes) needs to be the same size as the pointer was allocated with. + /// (Because similar to alignment, [`dealloc`] must be called with the + /// same layout `size`.) + /// - `length` needs to be less than or equal to `capacity`. + /// - The first `length` values must be properly initialized values of type + /// `T`. + /// - `capacity` needs to [*fit*] the layout size that the pointer was + /// allocated with. + /// - The allocated size in bytes must be no larger than `isize::MAX`. See + /// the safety documentation of [`std::pointer::offset`]. + /// + /// These requirements are always upheld by any `ptr` that has been + /// allocated via any `SmallVec`. Other allocation sources are + /// allowed if the invariants are upheld. + /// + /// Additionally, `capacity` must be greater than `N`; that is, the new + /// `SmallVec` must need to spill over into heap allocated storage. This + /// condition is asserted against. + /// + /// Violating these may cause problems like corrupting the allocator’s + /// internal data structures. + /// + /// The ownership of `ptr` is effectively transferred to the `SmallVec` which may then deallocate, reallocate or change the contents of + /// memory pointed to by the pointer at will. Ensure that nothing else + /// uses the pointer after calling this function. + /// + /// [*currently allocated*]: std::alloc::Allocator#currently-allocated-memory + /// [*fit*]: std::alloc::Allocator#memory-fitting + /// + /// # Examples + /// + /// ``` + /// use smallvec::SmallVec; + /// + /// let mut v: SmallVec<_, 1> = SmallVec::from([1, 2, 3]); + /// + /// // Pull out the important parts of `v` + /// let spilled = v.spilled(); + /// let (ptr, length, capacity) = v.into_parts(); + /// + /// unsafe { + /// // Overwrite memory with [4, 5, 6]. + /// // + /// // This is only safe if `spilled` is true! Otherwise, we are + /// // writing into the old `SmallVec`'s inline storage on the + /// // stack. + /// assert!(spilled); + /// for i in 0..length { + /// ptr.add(i).write(4 + i); + /// } + /// + /// // Put everything back together into a SmallVec with a different + /// // amount of inline storage, but which is still less than `capacity`. + /// let rebuilt = SmallVec::<_, 2>::from_parts(ptr, length, capacity); + /// assert_eq!(rebuilt, &[4, 5, 6]); + /// } + /// ``` + #[inline] + pub unsafe fn from_parts_in( + ptr: NonNull, + length: usize, + capacity: usize, + allocator: A + ) -> SmallVec { + assert!(!Self::IS_ZST); + + SmallVec { + length: LocatedLength::new(length, true), + raw: RawSmallVec::new_heap(ptr, capacity), + allocator + } + } + + /// Decomposes a `SmallVec` into its raw components: + /// `(NonNull pointer, length, capacity, allocator)`. + /// + /// Returns the `NonNull` pointer to the underlying data, the length of + /// the vector (in elements), the allocated capacity of the + /// data (in elements), and the allocator. These are the same arguments + /// in the same order as the arguments to [`from_parts_in`]. /// /// After calling this function, the caller is responsible for the /// memory previously managed by the `SmallVec`. Most often, one does /// this by converting the raw pointer, length, and capacity back - /// into a `SmallVec` with the [`from_raw_parts`] function; more generally, - /// if `T` is non-zero-sized and the capacity is nonzero, one may use - /// any method that calls [`dealloc`] with a layout of + /// into a `SmallVec` with the [`from_parts_in`] function, allowing the + /// destructor to perform the cleanup; more generally, + /// if `T` is non-zero-sized and the capacity is non-zero, one may use + /// any method that calls [`A::dealloc`] with a layout of /// `Layout::array::(capacity)`; if `T` is zero-sized or the /// capacity is zero, nothing needs to be done. /// - /// [`from_raw_parts`]: SmallVec::from_raw_parts - /// [`dealloc`]: alloc::alloc::GlobalAlloc::dealloc + /// [`from_parts_in`]: SmallVec::from_parts_in /// /// # Panics /// @@ -1233,25 +1416,25 @@ impl SmallVec { /// # use smallvec::SmallVec; /// let v: SmallVec = SmallVec::from([-1, 0, 1]); /// - /// let (ptr, length, cap) = v.into_raw_parts(); + /// let (ptr, length, cap, alloc) = v.into_parts_with_alloc(); /// /// let rebuilt = unsafe { /// // We can now make changes to the components, such as /// // transmuting the raw pointer to a compatible type. - /// let ptr = ptr as *mut u32; + /// let ptr = ptr.cast::(); /// - /// SmallVec::::from_raw_parts(ptr, length, cap) + /// SmallVec::::from_parts_in(ptr, length, cap, alloc) /// }; /// assert_eq!(rebuilt, [4294967295, 0, 1]); /// ``` #[inline] - pub fn into_raw_parts(self) -> (*mut T, usize, usize) { + pub fn into_parts_with_alloc(self) -> (NonNull, usize, usize, A) { #[cold] #[inline(never)] #[track_caller] fn assert_failed() -> ! { panic!( - "SmallVec::into_raw_parts() called on inline (stack) SmallVec, \ + "into_parts called on inline (stack) SmallVec, \ which cannot be safely leaked" ); } @@ -1260,7 +1443,15 @@ impl SmallVec { } let mut me = ManuallyDrop::new(self); - (me.as_mut_ptr(), me.len(), me.capacity()) + // SAFETY: The `SmallVec` is spilled. + unsafe { + ( + NonNull::new_unchecked(me.as_mut_ptr()), + me.len(), + me.capacity(), + (&raw const me.allocator).read() + ) + } } #[inline] @@ -1909,4 +2100,8 @@ impl Format for SmallVec { } #[cfg(feature = "encase")] -encase::rts_array::impl_rts_array!(SmallVec; (T, const N: usize, A: Allocator); using len truncate); +encase::rts_array::impl_rts_array!( + SmallVec; + (T, const N: usize, A: Allocator); + using len truncate +); diff --git a/tests/allocation.rs b/tests/allocation.rs new file mode 100644 index 00000000..ffabfe68 --- /dev/null +++ b/tests/allocation.rs @@ -0,0 +1,621 @@ +#![cfg(feature = "allocator-api")] + +extern crate alloc; +#[cfg(not(feature = "allocator-api2"))] +use alloc::alloc::{ + AllocError, + Allocator, + Layout +}; +#[cfg(feature = "allocator-api2")] +use allocator_api2::alloc::{ + AllocError, + Allocator, + Layout +}; +#[cfg(feature = "allocator-api2")] +use std::alloc::Allocator as _; +use { + alloc::alloc::Global, + core::{ + assert_matches, + cell::Cell, + ptr::NonNull + }, + smallvec::{ + SmallVec, + SmallVecError + } +}; + +#[derive(Clone, Copy, Debug, Default)] +struct AllocStats { + pub alloc_calls: usize, + pub dealloc_calls: usize, + pub grow_calls: usize, + pub shrink_calls: usize, + pub allocated_bytes: isize, + pub alloc_align: usize, + pub dealloc_align: usize +} + +#[derive(Clone, Debug, Default)] +struct TestAlloc { + stats: Cell +} + +impl TestAlloc { + #[inline] + fn new() -> Self { + Self::default() + } + + #[inline] + fn stats(&self) -> AllocStats { + self.stats.get() + } + + #[inline] + fn is_clean(&self) -> bool { + let s = self.stats.get(); + s.alloc_calls == s.dealloc_calls && s.allocated_bytes == 0 + } +} + +unsafe impl Allocator for TestAlloc { + fn allocate(&self, layout: Layout) -> Result, AllocError> { + let ptr = Global.allocate(layout).map_err(|_| AllocError)?; + self.stats.update(|mut s| { + s.alloc_calls += 1; + s.allocated_bytes += layout.size() as isize; + s.alloc_align = layout.align(); + s + }); + Ok(ptr) + } + + unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + self.stats.update(|mut s| { + s.dealloc_calls += 1; + s.allocated_bytes -= layout.size() as isize; + s.dealloc_align = layout.align(); + s + }); + unsafe { Global.deallocate(ptr, layout) }; + } + + unsafe fn grow( + &self, + ptr: NonNull, + old: Layout, + new: Layout + ) -> Result, AllocError> { + let out = unsafe { Global.grow(ptr, old, new) }.map_err(|_| AllocError)?; + self.stats.update(|mut s| { + s.grow_calls += 1; + s.allocated_bytes += new.size() as isize - old.size() as isize; + s + }); + Ok(out) + } + + unsafe fn shrink( + &self, + ptr: NonNull, + old: Layout, + new: Layout + ) -> Result, AllocError> { + let out = unsafe { Global.shrink(ptr, old, new) }.map_err(|_| AllocError)?; + self.stats.update(|mut s| { + s.shrink_calls += 1; + s.allocated_bytes -= old.size() as isize - new.size() as isize; + s + }); + Ok(out) + } +} + +// An allocator that delegates to [`TestAlloc`] but fails once it has handed +// out `remaining` successful allocations. Used to verify the `try_*` +// recovery paths. +#[derive(Clone, Debug, Default)] +struct FailingAlloc { + inner: TestAlloc, + remaining: Cell +} + +impl FailingAlloc { + #[inline] + fn new(remaining: usize) -> Self { + Self { + inner: TestAlloc::new(), + remaining: Cell::new(remaining) + } + } + + #[inline] + fn stats(&self) -> AllocStats { + self.inner.stats() + } + + #[inline] + fn is_clean(&self) -> bool { + self.inner.is_clean() + } +} + +unsafe impl Allocator for FailingAlloc { + fn allocate(&self, layout: Layout) -> Result, AllocError> { + let remaining = self.remaining.get(); + if remaining == 0 { + return Err(AllocError); + } + self.remaining.set(remaining - 1); + self.inner.allocate(layout) + } + + #[inline] + unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + unsafe { self.inner.deallocate(ptr, layout) } + } +} + +#[test] +fn test_inline_ops_do_not_allocate() { + const INLINE_SIZE: usize = 8; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::new_in(&alloc); + vec.extend(0..INLINE_SIZE as i32); + assert!(!vec.spilled()); + + vec.pop(); + vec.push(69); + vec.remove(2); + vec.insert(0, 2); + vec.swap_remove(0); + vec.fill(420); + vec.retain(|&x| x % 2 == 0); + vec.truncate(3); + drop(vec.drain(..1)); + vec.clear(); + + // `grow` within the inline capacity must not allocate + vec.grow(INLINE_SIZE); + + // `split_off` that stays inline must not allocate either + vec.extend(0..6); + let tail = vec.split_off(3); + assert_eq!(vec.as_slice(), &[0, 1, 2]); + assert_eq!(tail.as_slice(), &[3, 4, 5]); + + drop(vec); + drop(tail); + + assert_eq!(alloc.stats().alloc_calls, 0); + assert_eq!(alloc.stats().dealloc_calls, 0); + assert!(alloc.is_clean()); +} + +#[test] +fn test_spill_then_drop() { + const INLINE_SIZE: usize = 8; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::new_in(&alloc); + for i in 0..INLINE_SIZE as _ { + vec.push(i); + } + + // spill + vec.push(42); + // dealloc + drop(vec); + + assert_eq!(alloc.stats().alloc_calls, 1); + assert!(alloc.is_clean()); +} + +#[test] +fn test_with_capacity_in_allocates_properly() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + // allocate eagerly above inline size + let vec = SmallVec::::with_capacity_in(INLINE_SIZE + 1, &alloc); + drop(vec); + + assert_eq!(alloc.stats().alloc_calls, 1); + assert!(alloc.is_clean()); + + let vec = SmallVec::::with_capacity_in(INLINE_SIZE, &alloc); + drop(vec); + + assert_eq!(alloc.stats().alloc_calls, 1); + assert!(alloc.is_clean()); +} + +#[test] +fn test_moving_does_not_deallocate() { + const INLINE_SIZE: usize = 4; + + let alloc_a = TestAlloc::new(); + let alloc_b = TestAlloc::new(); + + let vec = SmallVec::::with_capacity_in(INLINE_SIZE + 1, &alloc_a); + let (ptr, len, cap, allocator) = vec.into_parts_with_alloc(); + + // moving to a different inline size must not deallocate + let vec = unsafe { + SmallVec::::from_parts_in(ptr, len, cap, allocator) + }; + drop(vec); + + // and the deallocation must go through `alloc_a`, not `alloc_b` + assert_eq!(alloc_a.stats().alloc_calls, 1); + assert_eq!(alloc_a.stats().dealloc_calls, 1); + assert!(alloc_a.is_clean()); + assert_eq!(alloc_b.stats().alloc_calls, 0); + assert_eq!(alloc_b.stats().dealloc_calls, 0); +} + +#[test] +fn test_unspill_deallocates() { + const INLINE_SIZE: usize = 2; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::new_in(&alloc); + for i in 0..(INLINE_SIZE + 1) as _ { + vec.push(i); + } + + assert!(vec.spilled()); + + vec.remove(1); + vec.shrink_to_fit(); + + assert!(!vec.spilled()); + assert_eq!(alloc.stats().alloc_calls, 1); + assert_eq!(alloc.stats().dealloc_calls, 1); + + drop(vec); + + assert_eq!(alloc.stats().dealloc_calls, 1); + assert!(alloc.is_clean()); +} + +#[test] +fn test_capacity_overflow_preserves_vec() { + const INLINE_SIZE: usize = 6; + + let alloc = TestAlloc::new(); + + // `try_grow` overflow on a spilled vec + let mut vec = SmallVec::::new_in(&alloc); + for i in 0..(INLINE_SIZE + 1) as _ { + vec.push(i); + } + assert_eq!(alloc.stats().alloc_calls, 1); + + assert_eq!( + vec.try_grow((isize::MAX as usize) + 4), + Err(SmallVecError::CapacityOverflow) + ); + assert_eq!( + vec.as_slice(), + core::array::from_fn::(|i| i as _) + ); + assert_eq!(alloc.stats().alloc_calls, 1); + + drop(vec); + + // `try_reserve` overflow on an inline vec + let mut vec = SmallVec::::new_in(&alloc); + vec.push(1); + assert_eq!( + vec.try_reserve(usize::MAX), + Err(SmallVecError::CapacityOverflow) + ); + assert_eq!(vec.as_slice(), &[1]); + assert_eq!(alloc.stats().alloc_calls, 1); + + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_no_alloc_on_zst() { + const INLINE_SIZE: usize = 2; + + let alloc = TestAlloc::new(); + + // pushing past the inline capacity must not allocate + let mut vec = SmallVec::<(), INLINE_SIZE, &TestAlloc>::new_in(&alloc); + for _ in 0..(INLINE_SIZE + 1) as _ { + vec.push(()); + } + drop(vec); + + // neither must requesting a large capacity + let vec = SmallVec::<(), INLINE_SIZE, &TestAlloc>::with_capacity_in(1024, &alloc); + assert_eq!(vec.capacity(), usize::MAX); + drop(vec); + + assert_eq!(alloc.stats().alloc_calls, 0); + assert_eq!(alloc.stats().dealloc_calls, 0); + assert!(alloc.is_clean()); +} + +#[test] +fn test_grow_reuses_allocation() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::new_in(&alloc); + for i in 0..(INLINE_SIZE + 1) as i32 { + vec.push(i); + } + assert_eq!(alloc.stats().alloc_calls, 1); + + // growing an already-spilled vec must realloc, not allocate a fresh block + vec.reserve(INLINE_SIZE * 4); + + assert_eq!(alloc.stats().alloc_calls, 1); + assert_eq!(alloc.stats().grow_calls, 1); + assert_eq!(alloc.stats().dealloc_calls, 0); + + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_shrink_to_fit_reuses_allocation() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::with_capacity_in(64, &alloc); + vec.extend(0..8); // still above `INLINE_SIZE` + + vec.shrink_to_fit(); + + let s = alloc.stats(); + assert_eq!(s.shrink_calls, 1); + assert_eq!(s.alloc_calls, 1); + assert_eq!(s.dealloc_calls, 0); + + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_shrink_to_unspills() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::with_capacity_in(16, &alloc); + vec.extend(0..2); + assert!(vec.spilled()); + + // should be at most 2, so it should inline again. + vec.shrink_to(0); + + assert!(!vec.spilled()); + assert_eq!(vec.as_slice(), &[0, 1]); + assert_eq!(alloc.stats().alloc_calls, 1); + assert_eq!(alloc.stats().dealloc_calls, 1); + + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_shrink_to_reallocates() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::with_capacity_in(64, &alloc); + vec.extend(0..8); + assert!(vec.spilled()); + + // should be at most 8, so it should remain spilled and shrink. + vec.shrink_to(8); + + assert!(vec.spilled()); + assert_eq!(vec.capacity(), 8); + assert_eq!(vec.as_slice(), &[0, 1, 2, 3, 4, 5, 6, 7]); + assert_eq!(alloc.stats().shrink_calls, 1); + assert_eq!(alloc.stats().alloc_calls, 1); + assert_eq!(alloc.stats().dealloc_calls, 0); + + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_shrink_to_noop() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::with_capacity_in(8, &alloc); + vec.extend(0..8); + + let before = alloc.stats(); + vec.shrink_to(8); // capacity == min, so no-op + vec.shrink_to(100); // capacity < min, so no-op + let after = alloc.stats(); + + assert_eq!(before.alloc_calls, after.alloc_calls); + assert_eq!(before.dealloc_calls, after.dealloc_calls); + assert_eq!(after.shrink_calls, 0); + assert_eq!(vec.capacity(), 8); + + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_growth_is_amortized() { + const INLINE_SIZE: usize = 4; + const N: usize = 1024; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::new_in(&alloc); + for i in 0..N as i32 { + vec.push(i); + } + assert_eq!(vec.len(), N); + + // only the initial spill allocates; every later growth reallocates + assert_eq!(alloc.stats().alloc_calls, 1); + // doubling growth here means about less than 16 allocations + assert!(alloc.stats().grow_calls <= 16); + + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_clone_allocates_and_clone_from_reuses() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::new_in(&alloc); + vec.extend(0..8); + assert!(vec.spilled()); + assert_eq!(alloc.stats().alloc_calls, 1); + + // cloning a spilled vec allocates + let clone = vec.clone(); + assert_eq!(clone.as_slice(), vec.as_slice()); + assert_eq!(alloc.stats().alloc_calls, 2); + + // clone_from into a vec with enough capacity must not allocate + let mut target = SmallVec::::with_capacity_in(8, &alloc); + let before = alloc.stats().alloc_calls; + target.clone_from(&vec); + assert_eq!(target.as_slice(), vec.as_slice()); + assert_eq!(alloc.stats().alloc_calls, before); + + drop(clone); + drop(target); + drop(vec); + assert!(alloc.is_clean()); +} + +#[test] +fn test_drop_correctness() { + use core::cell::Cell; + + struct DropCounter<'a>(&'a Cell); + impl Drop for DropCounter<'_> { + fn drop(&mut self) { + self.0.set(self.0.get() + 1); + } + } + + const INLINE_SIZE: usize = 2; + + let alloc = TestAlloc::new(); + let counter = Cell::new(0); + + { + let mut vec = SmallVec::, INLINE_SIZE, &TestAlloc>::new_in(&alloc); + for _ in 0..6 { + vec.push(DropCounter(&counter)); + } + assert!(vec.spilled()); + assert_eq!(counter.get(), 0); + + vec.truncate(4); + assert_eq!(counter.get(), 2); + + vec.clear(); + assert_eq!(counter.get(), 6); + } + + assert_eq!(counter.get(), 6); + assert!(alloc.is_clean()); +} + +#[test] +fn test_alignment_is_respected() { + #[repr(align(64))] + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + struct Aligned(u8); + + const INLINE_SIZE: usize = 2; + + let alloc = TestAlloc::new(); + + let mut vec = SmallVec::::new_in(&alloc); + for _ in 0..(INLINE_SIZE + 1) { + vec.push(Aligned(1)); + } + assert!(vec.spilled()); + assert_eq!(vec[0].0, 1); + + assert_eq!(alloc.stats().alloc_align, 64); + + drop(vec); + assert_eq!(alloc.stats().dealloc_align, 64); + assert!(alloc.is_clean()); +} + +#[test] +#[should_panic] +fn test_into_parts_with_alloc_inline_panics() { + const INLINE_SIZE: usize = 4; + + let alloc = TestAlloc::new(); + + let vec = SmallVec::::new_in(&alloc); + let _ = vec.into_parts_with_alloc(); +} + +#[test] +fn test_try_with_capacity_in_failure() { + const INLINE_SIZE: usize = 4; + + let alloc = FailingAlloc::new(0); + + let result = SmallVec::::try_with_capacity_in(8, &alloc); + assert_matches!(result, Err(SmallVecError::AllocationError(_))); + assert!(alloc.is_clean()); +} + +#[test] +fn test_allocation_failure_preserves_vec() { + const INLINE_SIZE: usize = 4; + + // allow the spill, fail the next allocation + let alloc = FailingAlloc::new(1); + + let mut vec = SmallVec::::new_in(&alloc); + for i in 0..(INLINE_SIZE + 1) as i32 { + vec.push(i); + } + assert!(vec.spilled()); + assert_eq!(alloc.stats().alloc_calls, 1); + + // both entry points must fail without touching the vec + assert_matches!(vec.try_grow(64), Err(SmallVecError::AllocationError(_))); + assert_matches!(vec.try_reserve(100), Err(SmallVecError::AllocationError(_))); + + assert_eq!(vec.as_slice(), &[0, 1, 2, 3, 4]); + assert!(vec.spilled()); + assert_eq!(alloc.stats().alloc_calls, 1); + + drop(vec); + assert!(alloc.is_clean()); +} diff --git a/tests/main.rs b/tests/main.rs index 22ecfb9f..5cae8385 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -773,20 +773,21 @@ fn shrink_after_from_empty_vec() { #[test] #[should_panic] -fn into_raw_parts_inline() { +fn into_parts_inline() { let v: SmallVec = SmallVec::from([1, 2, 3]); - v.into_raw_parts(); + v.into_parts(); } #[test] -fn into_raw_parts_heap() { +fn into_parts_heap() { let v: SmallVec = SmallVec::from([1, 2, 3]); - let (ptr, length, capacity) = v.into_raw_parts(); + let (ptr, length, capacity) = v.into_parts(); // It should be safe to create a standard `Vec` from the result. // SAFETY: allocated using `Global`, no changes to the element type. unsafe { - Vec::from_raw_parts(ptr, length, capacity); + // `allocator_api2::vec::Vec` doesn't expose `from_parts` yet + Vec::from_raw_parts(ptr.as_ptr(), length, capacity); } }