Skip to main content

vortex_buffer/
allocation.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Allocator-backed storage for Vortex buffers.
5
6use std::alloc::Layout;
7use std::any::Any;
8use std::fmt;
9use std::fmt::Debug;
10use std::mem::ManuallyDrop;
11use std::ptr::NonNull;
12use std::sync::Arc;
13
14use allocator_api2::alloc::AllocError;
15use allocator_api2::alloc::Allocator;
16use allocator_api2::alloc::Global;
17use allocator_api2::alloc::handle_alloc_error;
18use vortex_error::VortexExpect;
19
20use crate::Alignment;
21use crate::BufferMut;
22
23/// An allocator that can back a Vortex buffer.
24///
25/// Vortex over-allocates raw storage and aligns the buffer within it.
26pub trait BufferAllocator: Allocator + Debug + Send + Sync + 'static {}
27
28impl<A> BufferAllocator for A where A: Allocator + Debug + Send + Sync + 'static {}
29
30/// A shared reference to a buffer allocator.
31///
32/// The static allocator does not need shared ownership, so it is stored without an [`Arc`]. This
33/// makes cloning the common static allocator a simple value copy.
34#[derive(Clone)]
35pub struct BufferAllocatorRef(
36    // `None` selects the static allocator without allocating or updating an Arc reference count.
37    // `Some` keeps a custom allocator alive for as long as its buffers need it.
38    Option<Arc<dyn BufferAllocator>>,
39);
40
41impl BufferAllocatorRef {
42    /// Wrap an allocator in a shared reference.
43    pub fn new(allocator: impl BufferAllocator) -> Self {
44        Self(Some(Arc::new(allocator)))
45    }
46
47    /// Return a shared reference to the static allocator.
48    pub fn statically_allocated() -> Self {
49        Self(None)
50    }
51
52    /// Return a borrowed reference to the static allocator.
53    pub fn static_ref() -> &'static Self {
54        &STATIC_ALLOCATOR
55    }
56
57    pub(crate) fn is_statically_allocated(&self) -> bool {
58        self.0.is_none()
59    }
60
61    /// Returns true if both references point to the same allocator.
62    pub fn ptr_eq(&self, other: &Self) -> bool {
63        match (&self.0, &other.0) {
64            (None, None) => true,
65            (Some(lhs), Some(rhs)) => Arc::ptr_eq(lhs, rhs),
66            _ => false,
67        }
68    }
69
70    /// Create a mutable buffer with this allocator.
71    pub fn with_capacity<T>(&self, capacity: usize) -> BufferMut<T> {
72        BufferMut::with_capacity_in(capacity, self.clone())
73    }
74
75    /// Create an aligned mutable buffer with this allocator.
76    pub fn with_capacity_aligned<T>(&self, capacity: usize, alignment: Alignment) -> BufferMut<T> {
77        BufferMut::with_capacity_aligned_in(capacity, alignment, self.clone())
78    }
79
80    /// Create a zeroed mutable buffer with this allocator.
81    pub fn zeroed<T>(&self, len: usize) -> BufferMut<T> {
82        BufferMut::zeroed_in(len, self.clone())
83    }
84
85    /// Copy values into a mutable buffer made by this allocator.
86    pub fn copy_from<T>(&self, values: impl AsRef<[T]>) -> BufferMut<T> {
87        BufferMut::copy_from_in(values, self.clone())
88    }
89}
90
91impl Debug for BufferAllocatorRef {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match &self.0 {
94            Some(allocator) => allocator.fmt(f),
95            None => StaticBufferAllocator.fmt(f),
96        }
97    }
98}
99
100// SAFETY: all calls are forwarded to the same allocator value held by the Arc.
101unsafe impl Allocator for BufferAllocatorRef {
102    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
103        match &self.0 {
104            Some(allocator) => allocator.allocate(layout),
105            None => Global.allocate(layout),
106        }
107    }
108
109    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
110        match &self.0 {
111            Some(allocator) => allocator.allocate_zeroed(layout),
112            None => Global.allocate_zeroed(layout),
113        }
114    }
115
116    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
117        // SAFETY: the caller upholds the Allocator contract.
118        match &self.0 {
119            Some(allocator) => unsafe { allocator.deallocate(ptr, layout) },
120            None => unsafe { Global.deallocate(ptr, layout) },
121        }
122    }
123
124    unsafe fn grow(
125        &self,
126        ptr: NonNull<u8>,
127        old_layout: Layout,
128        new_layout: Layout,
129    ) -> Result<NonNull<[u8]>, AllocError> {
130        // SAFETY: the caller upholds the Allocator contract.
131        match &self.0 {
132            Some(allocator) => unsafe { allocator.grow(ptr, old_layout, new_layout) },
133            None => unsafe { Global.grow(ptr, old_layout, new_layout) },
134        }
135    }
136
137    unsafe fn grow_zeroed(
138        &self,
139        ptr: NonNull<u8>,
140        old_layout: Layout,
141        new_layout: Layout,
142    ) -> Result<NonNull<[u8]>, AllocError> {
143        // SAFETY: the caller upholds the Allocator contract.
144        match &self.0 {
145            Some(allocator) => unsafe { allocator.grow_zeroed(ptr, old_layout, new_layout) },
146            None => unsafe { Global.grow_zeroed(ptr, old_layout, new_layout) },
147        }
148    }
149
150    unsafe fn shrink(
151        &self,
152        ptr: NonNull<u8>,
153        old_layout: Layout,
154        new_layout: Layout,
155    ) -> Result<NonNull<[u8]>, AllocError> {
156        // SAFETY: the caller upholds the Allocator contract.
157        match &self.0 {
158            Some(allocator) => unsafe { allocator.shrink(ptr, old_layout, new_layout) },
159            None => unsafe { Global.shrink(ptr, old_layout, new_layout) },
160        }
161    }
162}
163
164/// The allocator used by buffer APIs that do not take an allocator.
165#[derive(Clone, Copy, Debug, Default)]
166pub struct StaticBufferAllocator;
167
168impl StaticBufferAllocator {
169    /// Create a mutable buffer with the static allocator.
170    pub fn with_capacity<T>(capacity: usize) -> BufferMut<T> {
171        BufferMut::with_capacity(capacity)
172    }
173
174    /// Create an aligned mutable buffer with the static allocator.
175    pub fn with_capacity_aligned<T>(capacity: usize, alignment: Alignment) -> BufferMut<T> {
176        BufferMut::with_capacity_aligned(capacity, alignment)
177    }
178
179    /// Create a zeroed mutable buffer with the static allocator.
180    pub fn zeroed<T>(len: usize) -> BufferMut<T> {
181        BufferMut::zeroed(len)
182    }
183
184    /// Copy values into a mutable buffer made by the static allocator.
185    pub fn copy_from<T>(values: impl AsRef<[T]>) -> BufferMut<T> {
186        BufferMut::copy_from(values)
187    }
188}
189
190// SAFETY: Global satisfies the Allocator contract and this type only forwards to it.
191unsafe impl Allocator for StaticBufferAllocator {
192    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
193        Global.allocate(layout)
194    }
195
196    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
197        Global.allocate_zeroed(layout)
198    }
199
200    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
201        // SAFETY: the caller upholds the Allocator contract.
202        unsafe { Global.deallocate(ptr, layout) }
203    }
204
205    unsafe fn grow(
206        &self,
207        ptr: NonNull<u8>,
208        old_layout: Layout,
209        new_layout: Layout,
210    ) -> Result<NonNull<[u8]>, AllocError> {
211        // SAFETY: the caller upholds the Allocator contract.
212        unsafe { Global.grow(ptr, old_layout, new_layout) }
213    }
214
215    unsafe fn grow_zeroed(
216        &self,
217        ptr: NonNull<u8>,
218        old_layout: Layout,
219        new_layout: Layout,
220    ) -> Result<NonNull<[u8]>, AllocError> {
221        // SAFETY: the caller upholds the Allocator contract.
222        unsafe { Global.grow_zeroed(ptr, old_layout, new_layout) }
223    }
224
225    unsafe fn shrink(
226        &self,
227        ptr: NonNull<u8>,
228        old_layout: Layout,
229        new_layout: Layout,
230    ) -> Result<NonNull<[u8]>, AllocError> {
231        // SAFETY: the caller upholds the Allocator contract.
232        unsafe { Global.shrink(ptr, old_layout, new_layout) }
233    }
234}
235
236static STATIC_ALLOCATOR: BufferAllocatorRef = BufferAllocatorRef(None);
237
238pub(crate) struct Allocation {
239    ptr: NonNull<u8>,
240    layout: Layout,
241    allocator: BufferAllocatorRef,
242}
243
244// SAFETY: Allocation owns its memory, and its allocator is Send + Sync.
245unsafe impl Send for Allocation {}
246// SAFETY: shared access to Allocation never permits mutation of the allocation.
247unsafe impl Sync for Allocation {}
248
249impl Allocation {
250    pub(crate) fn allocate(layout: Layout, allocator: BufferAllocatorRef) -> Self {
251        Self::allocate_impl(layout, allocator, false)
252    }
253
254    pub(crate) fn allocate_zeroed(layout: Layout, allocator: BufferAllocatorRef) -> Self {
255        Self::allocate_impl(layout, allocator, true)
256    }
257
258    pub(crate) fn from_vec<T>(vec: Vec<T>) -> Self {
259        assert!(!std::mem::needs_drop::<T>());
260
261        let mut vec = ManuallyDrop::new(vec);
262        let layout = Layout::array::<T>(vec.capacity())
263            .unwrap_or_else(|_| unreachable!("a Vec capacity always has a valid layout"));
264        let ptr = NonNull::new(vec.as_mut_ptr().cast())
265            .vortex_expect("a Vec always has a non-null pointer");
266
267        Self {
268            ptr,
269            layout,
270            allocator: BufferAllocatorRef::statically_allocated(),
271        }
272    }
273
274    fn allocate_impl(layout: Layout, allocator: BufferAllocatorRef, zeroed: bool) -> Self {
275        if layout.size() == 0 {
276            return Self {
277                ptr: layout.dangling_ptr(),
278                layout,
279                allocator,
280            };
281        }
282
283        let allocation = if zeroed {
284            allocator.allocate_zeroed(layout)
285        } else {
286            allocator.allocate(layout)
287        }
288        .unwrap_or_else(|_| handle_alloc_error(layout));
289
290        Self {
291            ptr: allocation.cast(),
292            layout,
293            allocator,
294        }
295    }
296
297    #[allow(clippy::inline_always)]
298    #[inline(always)]
299    pub(crate) fn ptr(&self) -> NonNull<u8> {
300        self.ptr
301    }
302
303    #[allow(clippy::inline_always)]
304    #[inline(always)]
305    pub(crate) fn size(&self) -> usize {
306        self.layout.size()
307    }
308
309    #[allow(clippy::inline_always)]
310    #[inline(always)]
311    pub(crate) fn alignment(&self) -> usize {
312        self.layout.align()
313    }
314
315    #[allow(clippy::inline_always)]
316    #[inline(always)]
317    pub(crate) fn allocator(&self) -> &BufferAllocatorRef {
318        &self.allocator
319    }
320
321    pub(crate) fn grow(&mut self, new_layout: Layout) {
322        let allocation = if self.layout.size() == 0 {
323            self.allocator.allocate(new_layout)
324        } else {
325            // SAFETY: ptr denotes a live block owned by allocator, old_layout fits the block, and
326            // the caller only grows the allocation.
327            unsafe { self.allocator.grow(self.ptr, self.layout, new_layout) }
328        }
329        .unwrap_or_else(|_| handle_alloc_error(new_layout));
330        self.ptr = allocation.cast();
331        self.layout = new_layout;
332    }
333}
334
335impl Drop for Allocation {
336    fn drop(&mut self) {
337        if self.layout.size() == 0 {
338            return;
339        }
340        // SAFETY: ptr and layout describe a live block allocated by self.allocator.
341        unsafe { self.allocator.deallocate(self.ptr, self.layout) }
342    }
343}
344
345pub(crate) enum BufferBacking {
346    Owned(Allocation),
347    Bytes(bytes::Bytes),
348    #[cfg(feature = "arrow")]
349    Arrow(arrow_buffer::Buffer),
350    External {
351        _owner: Box<dyn Any + Send + Sync>,
352    },
353}
354
355impl BufferBacking {
356    #[allow(clippy::inline_always)]
357    #[inline(always)]
358    pub(crate) fn allocator(&self) -> &BufferAllocatorRef {
359        match self {
360            Self::Owned(allocation) => allocation.allocator(),
361            Self::Bytes(_) | Self::External { .. } => &STATIC_ALLOCATOR,
362            #[cfg(feature = "arrow")]
363            Self::Arrow(_) => &STATIC_ALLOCATOR,
364        }
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use std::alloc::Layout;
371    use std::ptr::NonNull;
372    use std::sync::Arc;
373    use std::sync::atomic::AtomicUsize;
374    use std::sync::atomic::Ordering;
375
376    use allocator_api2::alloc::AllocError;
377    use allocator_api2::alloc::Allocator;
378    use allocator_api2::alloc::Global;
379    use rstest::rstest;
380    use vortex_error::VortexResult;
381    use vortex_error::vortex_err;
382
383    use crate::Alignment;
384    use crate::BufferAllocatorRef;
385    use crate::BufferMut;
386
387    #[derive(Clone, Debug, Default)]
388    struct TrackingAllocator {
389        state: Arc<TrackingState>,
390    }
391
392    #[derive(Debug, Default)]
393    struct TrackingState {
394        allocations: AtomicUsize,
395        deallocations: AtomicUsize,
396        grows: AtomicUsize,
397        alignment: AtomicUsize,
398    }
399
400    // SAFETY: this forwards all memory operations to Global and only records call metadata.
401    unsafe impl Allocator for TrackingAllocator {
402        fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
403            self.state.allocations.fetch_add(1, Ordering::Relaxed);
404            self.state
405                .alignment
406                .store(layout.align(), Ordering::Relaxed);
407            Global.allocate(layout)
408        }
409
410        unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
411            self.state.deallocations.fetch_add(1, Ordering::Relaxed);
412            // SAFETY: the caller passes the pointer and layout returned by Global.
413            unsafe { Global.deallocate(ptr, layout) }
414        }
415
416        unsafe fn grow(
417            &self,
418            ptr: NonNull<u8>,
419            old_layout: Layout,
420            new_layout: Layout,
421        ) -> Result<NonNull<[u8]>, AllocError> {
422            self.state.grows.fetch_add(1, Ordering::Relaxed);
423            // SAFETY: the caller upholds the Allocator contract.
424            unsafe { Global.grow(ptr, old_layout, new_layout) }
425        }
426    }
427
428    #[test]
429    fn allocator_identity() {
430        let static_allocator = BufferAllocatorRef::statically_allocated();
431        assert!(static_allocator.ptr_eq(&BufferAllocatorRef::statically_allocated()));
432
433        let custom_allocator = BufferAllocatorRef::new(TrackingAllocator::default());
434        assert!(custom_allocator.ptr_eq(&custom_allocator.clone()));
435        assert!(!custom_allocator.ptr_eq(&static_allocator));
436        assert!(!custom_allocator.ptr_eq(&BufferAllocatorRef::new(TrackingAllocator::default())));
437    }
438
439    #[test]
440    fn allocation_lives_until_last_view() {
441        let allocator = TrackingAllocator::default();
442        let state = Arc::clone(&allocator.state);
443        let buffer = BufferAllocatorRef::new(allocator)
444            .copy_from([1u32, 2, 3, 4])
445            .freeze();
446        let view = buffer.slice(0..2);
447
448        assert_eq!(state.allocations.load(Ordering::Relaxed), 1);
449        assert_eq!(
450            state.alignment.load(Ordering::Relaxed),
451            Alignment::of::<u8>().as_usize()
452        );
453        drop(buffer);
454        assert_eq!(state.deallocations.load(Ordering::Relaxed), 0);
455        drop(view);
456        assert_eq!(state.deallocations.load(Ordering::Relaxed), 1);
457    }
458
459    #[rstest]
460    fn buffer_growth_uses_allocator_grow(#[values(4, 64, 4096)] alignment: usize) {
461        let allocator = TrackingAllocator::default();
462        let state = Arc::clone(&allocator.state);
463        let alignment = Alignment::new(alignment);
464        let mut buffer =
465            BufferAllocatorRef::new(allocator).with_capacity_aligned::<u32>(1, alignment);
466        let initial_capacity = buffer.capacity();
467        buffer.extend(std::iter::repeat_n(7, initial_capacity));
468
469        buffer.push(u32::MAX);
470        assert!(alignment.is_ptr_aligned(buffer.as_ptr()));
471
472        assert_eq!(&buffer[..initial_capacity], vec![7; initial_capacity]);
473        assert_eq!(buffer[initial_capacity], u32::MAX);
474        assert_eq!(state.allocations.load(Ordering::Relaxed), 1);
475        assert_eq!(state.deallocations.load(Ordering::Relaxed), 0);
476        assert_eq!(state.grows.load(Ordering::Relaxed), 1);
477
478        drop(buffer);
479        assert_eq!(state.deallocations.load(Ordering::Relaxed), 1);
480    }
481
482    #[test]
483    fn zero_capacity_does_not_allocate() {
484        let allocator = TrackingAllocator::default();
485        let state = Arc::clone(&allocator.state);
486        let mut buffer = BufferAllocatorRef::new(allocator).with_capacity::<u32>(0);
487
488        assert_eq!(buffer.capacity(), 0);
489        assert!(Alignment::DEFAULT_ALIGNMENT.is_offset_aligned(buffer.as_ptr().addr()));
490        assert_eq!(state.allocations.load(Ordering::Relaxed), 0);
491
492        buffer.push(42);
493
494        assert_eq!(buffer.as_slice(), [42]);
495        assert_eq!(state.allocations.load(Ordering::Relaxed), 1);
496        assert_eq!(state.grows.load(Ordering::Relaxed), 0);
497    }
498
499    #[test]
500    fn empty_buffers_preserve_allocator_without_allocating() -> VortexResult<()> {
501        let allocator = TrackingAllocator::default();
502        let state = Arc::clone(&allocator.state);
503        let allocator = BufferAllocatorRef::new(allocator);
504        let buffer = BufferMut::<u32>::zeroed_in(0, allocator.clone());
505        let buffer = buffer.freeze();
506        let copy = buffer.clone().into_mut();
507        assert!(copy.allocator().ptr_eq(&allocator));
508        let mut buffer = buffer
509            .try_into_mut()
510            .map_err(|_| vortex_err!("unique buffer"))?;
511        buffer.reserve(0);
512        assert!(buffer.is_empty());
513        assert!(buffer.allocator().ptr_eq(&allocator));
514        drop((copy, buffer));
515        assert_eq!(state.allocations.load(Ordering::Relaxed), 0);
516        assert_eq!(state.grows.load(Ordering::Relaxed), 0);
517        assert_eq!(state.deallocations.load(Ordering::Relaxed), 0);
518        Ok(())
519    }
520
521    #[test]
522    fn shared_into_mut_preserves_allocator() {
523        let allocator = TrackingAllocator::default();
524        let state = Arc::clone(&allocator.state);
525        let allocator = BufferAllocatorRef::new(allocator);
526        let original = allocator.copy_from([1u32, 2, 3]).freeze();
527        let mut copy = original.clone().into_mut();
528        assert!(copy.allocator().ptr_eq(&allocator));
529        copy[0] = 42;
530        assert_eq!(original.as_slice(), [1, 2, 3]);
531        assert_eq!(copy.as_slice(), [42, 2, 3]);
532        assert_eq!(state.allocations.load(Ordering::Relaxed), 2);
533        drop(copy);
534        assert_eq!(state.deallocations.load(Ordering::Relaxed), 1);
535        drop(original);
536        assert_eq!(state.deallocations.load(Ordering::Relaxed), 2);
537    }
538}