Skip to main content

low_latency_data_structures/
mem.rs

1//! Module for all things memory
2
3use std::mem::MaybeUninit;
4
5use crate::shim::alloc;
6use crate::shim::cell::UnsafeCell;
7
8/// Backing allocator for the queue slot buffer.
9///
10/// Implementors are called once at construction, before any producer or
11/// consumer touches the returned memory, and are expected to hand back a
12/// contiguous, aligned region big enough to hold `items` cells.
13pub trait Allocator {
14    /// Configuration passed to [`allocate`](Self::allocate). Each allocator
15    /// defines its own options type (e.g. mlock toggle, hugepage size).
16    type Options;
17    /// Allocation wrapper type returned by [`allocate`](Self::allocate).
18    type Allocation<T>: Allocation<T>;
19
20    /// Allocates space for `items` cells of `T`.
21    ///
22    /// The returned [`Allocation`] owns the memory and releases it on drop.
23    /// The cells may be uninitialized; callers must write a cell before
24    /// reading it.
25    fn allocate<T>(items: usize, options: Self::Options) -> Self::Allocation<T>;
26}
27
28/// Handle for a slot buffer produced by an [`Allocator`].
29///
30/// The pointer returned by [`ptr`](Self::ptr) stays valid for the lifetime of
31/// the `Allocation`; dropping it releases the underlying memory.
32pub trait Allocation<T> {
33    /// Base pointer of the allocated region. Points to `items` contiguous,
34    /// aligned cells (see [`Allocator::allocate`]).
35    fn ptr(&self) -> *mut UnsafeCell<MaybeUninit<T>>;
36}
37
38#[cfg(test)]
39pub(crate) mod test_util {
40    use std::marker::PhantomData;
41
42    use super::*;
43
44    /// A stub `Allocation` used only in `static_assertions` trait-bound
45    /// checks. Its `ptr()` must never be called.
46    pub(crate) struct NeverAllocation<T> {
47        _t: PhantomData<T>,
48    }
49
50    pub(crate) struct NeverAllocator;
51
52    impl Allocator for NeverAllocator {
53        type Options = ();
54        type Allocation<T> = NeverAllocation<T>;
55
56        fn allocate<T>(_items: usize, _options: Self::Options) -> Self::Allocation<T> {
57            unreachable!("NeverAllocator is a stub for trait checks only");
58        }
59    }
60
61    impl<T> Allocation<T> for NeverAllocation<T> {
62        fn ptr(&self) -> *mut UnsafeCell<MaybeUninit<T>> {
63            unreachable!("NeverAllocation is a stub for trait checks only")
64        }
65    }
66}
67
68/// [`Allocator`] backed by Linux hugepages via `mmap(MAP_HUGETLB)`.
69pub mod hugepages {
70    use super::*;
71
72    /// Linux hugepage size, selected via `MAP_HUGE_*` mmap flags.
73    #[derive(Clone, Copy)]
74    pub enum HugepageSize {
75        /// Allocates using libc mmap with hugepage size of 64KB
76        H64KB,
77        /// Allocates using libc mmap with hugepage size of 512KB
78        H512KB,
79        /// Allocates using libc mmap with hugepage size of 1MB
80        H1MB,
81        /// Allocates using libc mmap with hugepage size of 2MB
82        H2MB,
83        /// Allocates using libc mmap with hugepage size of 8MB
84        H8MB,
85        /// Allocates using libc mmap with hugepage size of 16MB
86        H16MB,
87        /// Allocates using libc mmap with hugepage size of 32MB
88        H32MB,
89        /// Allocates using libc mmap with hugepage size of 256MB
90        H256MB,
91        /// Allocates using libc mmap with hugepage size of 512MB
92        H512MB,
93    }
94
95    impl HugepageSize {
96        fn as_usize_bytes(&self) -> usize {
97            match self {
98                HugepageSize::H64KB => 64 * 1024,
99                HugepageSize::H512KB => 512 * 1024,
100                HugepageSize::H1MB => 1024 * 1024,
101                HugepageSize::H2MB => 2 * 1024 * 1024,
102                HugepageSize::H8MB => 8 * 1024 * 1024,
103                HugepageSize::H16MB => 16 * 1024 * 1024,
104                HugepageSize::H32MB => 32 * 1024 * 1024,
105                HugepageSize::H256MB => 256 * 1024 * 1024,
106                HugepageSize::H512MB => 512 * 1024 * 1024,
107            }
108        }
109
110        fn as_mmap_size_flag(&self) -> libc::c_int {
111            match self {
112                HugepageSize::H64KB => libc::MAP_HUGE_64KB,
113                HugepageSize::H512KB => libc::MAP_HUGE_512KB,
114                HugepageSize::H1MB => libc::MAP_HUGE_1MB,
115                HugepageSize::H2MB => libc::MAP_HUGE_2MB,
116                HugepageSize::H8MB => libc::MAP_HUGE_8MB,
117                HugepageSize::H16MB => libc::MAP_HUGE_16MB,
118                HugepageSize::H32MB => libc::MAP_HUGE_32MB,
119                HugepageSize::H256MB => libc::MAP_HUGE_256MB,
120                HugepageSize::H512MB => libc::MAP_HUGE_512MB,
121            }
122        }
123    }
124
125    /// Allocates via `mmap(MAP_HUGETLB | MAP_HUGE_<size>)`. Requires the
126    /// kernel to have enough free hugepages of the requested size.
127    ///
128    /// # Examples
129    ///
130    /// Build an SPSC queue backed by 2 MB hugepages with `mlock`:
131    ///
132    /// ```no_run
133    /// use low_latency_data_structures::mem::hugepages::{
134    ///     HugepageAllocator, HugepageAllocatorOptions, HugepageSize,
135    /// };
136    /// use low_latency_data_structures::spsc;
137    ///
138    /// let alloc = HugepageAllocatorOptions::builder()
139    ///     .mlock(true)
140    ///     .hugepage_size(HugepageSize::H2MB)
141    ///     .build();
142    /// let (producer, consumer) = spsc::new::<u64, 1024, HugepageAllocator>(
143    ///     spsc::Options::builder().alloc(alloc).build(),
144    /// );
145    /// # let _ = (producer, consumer);
146    /// ```
147    pub struct HugepageAllocator;
148
149    /// Options for [`HugepageAllocator`].
150    #[derive(bon::Builder)]
151    pub struct HugepageAllocatorOptions {
152        /// If true, `mlock` the allocated region so it stays resident.
153        mlock: bool,
154        /// Hugepage size passed to `mmap` via `MAP_HUGE_*` flags.
155        hugepage_size: HugepageSize,
156    }
157
158    impl Allocator for HugepageAllocator {
159        type Options = HugepageAllocatorOptions;
160        type Allocation<T> = HugepageAllocation<T>;
161
162        #[inline]
163        fn allocate<T>(items: usize, options: Self::Options) -> Self::Allocation<T> {
164            let layout =
165                alloc::Layout::array::<UnsafeCell<MaybeUninit<T>>>(items).unwrap_or_else(|_| {
166                    panic!(
167                        "Layout::array overflow: {items} x {} bytes exceeds isize::MAX",
168                        std::mem::size_of::<UnsafeCell<MaybeUninit<T>>>()
169                    )
170                });
171            let page_size = options.hugepage_size.as_usize_bytes();
172            // Round up to page size
173            let alloc_size = (layout.size() + page_size - 1) & !(page_size - 1);
174            let ptr = unsafe {
175                libc::mmap(
176                    std::ptr::null_mut(),
177                    alloc_size,
178                    libc::PROT_READ | libc::PROT_WRITE,
179                    libc::MAP_POPULATE
180                        | libc::MAP_PRIVATE
181                        | libc::MAP_ANONYMOUS
182                        | libc::MAP_HUGETLB
183                        | options.hugepage_size.as_mmap_size_flag(),
184                    -1,
185                    0,
186                )
187            };
188            assert_ne!(
189                ptr,
190                libc::MAP_FAILED,
191                "hugepage mmap failed: {}",
192                std::io::Error::last_os_error()
193            );
194            if options.mlock {
195                let rc = unsafe { libc::mlock(ptr.cast(), alloc_size) };
196                assert_ne!(
197                    rc,
198                    -1,
199                    "mlock hugepage allocation failed: {}",
200                    std::io::Error::last_os_error()
201                );
202            }
203            HugepageAllocation {
204                ptr: ptr as *mut UnsafeCell<MaybeUninit<T>>,
205                length: alloc_size,
206            }
207        }
208    }
209
210    /// Owned mmap'd hugepage region returned by
211    /// [`HugepageAllocator::allocate`]. Releases via `munmap` on drop.
212    pub struct HugepageAllocation<T> {
213        ptr: *mut UnsafeCell<MaybeUninit<T>>,
214        length: usize,
215    }
216
217    unsafe impl<T> Send for HugepageAllocation<T> where T: Send {}
218
219    impl<T> Drop for HugepageAllocation<T> {
220        fn drop(&mut self) {
221            let rc = unsafe { libc::munmap(self.ptr.cast(), self.length) };
222            assert_ne!(
223                rc,
224                -1,
225                "failed to munmap hugepage allocation: {}",
226                std::io::Error::last_os_error()
227            );
228        }
229    }
230
231    impl<T> Allocation<T> for HugepageAllocation<T> {
232        #[inline]
233        fn ptr(&self) -> *mut UnsafeCell<MaybeUninit<T>> {
234            self.ptr
235        }
236    }
237}
238
239/// [`Allocator`] backed by the process's `#[global_allocator]`.
240pub mod global {
241    use super::*;
242
243    /// Allocates via the process's `#[global_allocator]`. This is the
244    /// default choice when hugepages aren't available or wanted.
245    pub struct GlobalAllocator;
246
247    /// Options for [`GlobalAllocator`].
248    #[derive(bon::Builder)]
249    pub struct GlobalAllocatorOptions {
250        /// If true, `mlock` the allocated region so it stays resident.
251        mlock: bool,
252    }
253
254    impl Allocator for GlobalAllocator {
255        type Options = GlobalAllocatorOptions;
256        type Allocation<T> = GlobalAllocation<T>;
257
258        #[inline]
259        fn allocate<T>(items: usize, options: Self::Options) -> Self::Allocation<T> {
260            let layout =
261                alloc::Layout::array::<UnsafeCell<MaybeUninit<T>>>(items).unwrap_or_else(|_| {
262                    panic!(
263                        "Layout::array overflow: {items} x {} bytes exceeds isize::MAX",
264                        std::mem::size_of::<UnsafeCell<MaybeUninit<T>>>()
265                    )
266                });
267            let ptr = unsafe { alloc::alloc(layout) };
268            if ptr.is_null() {
269                alloc::handle_alloc_error(layout);
270            }
271            if options.mlock {
272                let rc = unsafe { libc::mlock(ptr.cast(), layout.size()) };
273                assert_ne!(
274                    rc,
275                    -1,
276                    "mlock global allocation failed: {}",
277                    std::io::Error::last_os_error()
278                );
279            }
280            GlobalAllocation {
281                ptr: ptr.cast(),
282                layout,
283            }
284        }
285    }
286
287    /// Owned global-allocator region returned by
288    /// [`GlobalAllocator::allocate`]. Releases via `dealloc` on drop.
289    pub struct GlobalAllocation<T> {
290        ptr: *mut UnsafeCell<MaybeUninit<T>>,
291        layout: alloc::Layout,
292    }
293
294    unsafe impl<T> Send for GlobalAllocation<T> where T: Send {}
295
296    impl<T> Drop for GlobalAllocation<T> {
297        fn drop(&mut self) {
298            unsafe { alloc::dealloc(self.ptr.cast(), self.layout) };
299        }
300    }
301
302    impl<T> Allocation<T> for GlobalAllocation<T> {
303        #[inline]
304        fn ptr(&self) -> *mut UnsafeCell<MaybeUninit<T>> {
305            self.ptr
306        }
307    }
308}
309
310#[cfg(test)]
311#[cfg(feature = "tests_loom")]
312pub(crate) mod loom {
313    use super::*;
314
315    pub(crate) struct LoomVecAllocator;
316
317    pub(crate) struct LoomVecAllocatorOptions;
318
319    impl Allocator for LoomVecAllocator {
320        type Options = LoomVecAllocatorOptions;
321        type Allocation<T> = LoomVecAllocation<T>;
322
323        #[inline]
324        fn allocate<T>(items: usize, _options: Self::Options) -> Self::Allocation<T> {
325            type RealT<T> = UnsafeCell<MaybeUninit<T>>;
326            let mut buffer: Vec<RealT<T>> = Vec::with_capacity(items);
327            for _ in 0..items {
328                buffer.push(UnsafeCell::new(MaybeUninit::uninit()));
329            }
330            let ptr = buffer.as_mut_ptr();
331            std::mem::forget(buffer);
332            LoomVecAllocation {
333                ptr,
334                capacity: items,
335            }
336        }
337    }
338
339    pub struct LoomVecAllocation<T> {
340        ptr: *mut UnsafeCell<MaybeUninit<T>>,
341        capacity: usize,
342    }
343
344    unsafe impl<T> Send for LoomVecAllocation<T> where T: Send {}
345
346    impl<T> Drop for LoomVecAllocation<T> {
347        fn drop(&mut self) {
348            unsafe { drop(Vec::from_raw_parts(self.ptr, self.capacity, self.capacity)) };
349        }
350    }
351
352    impl<T> Allocation<T> for LoomVecAllocation<T> {
353        #[inline]
354        fn ptr(&self) -> *mut UnsafeCell<MaybeUninit<T>> {
355            self.ptr
356        }
357    }
358}
359
360/// Type of backing allocation
361pub enum BackingAllocation {
362    /// Allocates using whatever `#[global_allocator]` is set to.
363    Global,
364    /// Allocates via `mmap(MAP_HUGETLB)` at the given hugepage size.
365    Hugepages(hugepages::HugepageSize),
366}