Skip to main content

low_latency_data_structures/spmc/
queue.rs

1use std::mem::MaybeUninit;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicUsize, Ordering, fence};
4
5use crate::mem::{Allocation, Allocator};
6use crate::shim::cell::UnsafeCell;
7use crate::spmc::builder::Options;
8use crate::spmc::consumer::Consumer;
9use crate::spmc::producer::{Producer, ProducerState, ProducerStateInner};
10
11/// Creates a new SPMC broadcast queue with `CAPACITY` slots and a single
12/// [`Consumer`]. Additional consumers can be obtained by cloning it; each
13/// clone inherits the parent's current read cursor.
14///
15/// `CAPACITY` must be a power of two (compile-time enforced). The queue is
16/// allocated up front and never grows. `T: AnyBitPattern` is required so
17/// that torn reads materialise a valid `T` rather than triggering undefined
18/// behaviour; the surrounding seq protocol then rejects the torn value
19/// before it leaves [`Consumer::try_read`].
20///
21/// # Examples
22///
23/// ```
24/// use low_latency_data_structures::spmc::{self, ReadResult, new};
25/// use low_latency_data_structures::mem::global::GlobalAllocator;
26///
27/// let (producer, mut a) = new::<u64, 4, GlobalAllocator>(
28///     spmc::Options::global_mlocked(),
29/// );
30/// let mut b = a.clone();
31/// producer.publish(1);
32/// assert_eq!(a.try_read(), ReadResult::Value(1));
33/// assert_eq!(b.try_read(), ReadResult::Value(1));
34/// ```
35///
36/// Capacities that are not powers of two fail to compile:
37///
38/// ```compile_fail
39/// # use low_latency_data_structures::spmc::{self, new};
40/// # use low_latency_data_structures::mem::global::GlobalAllocator;
41/// # use seq_macro::seq;
42/// seq!(N in 2..20 {
43///     {
44///         const CAP: usize = 2usize.wrapping_pow(N);
45///         let _fail = new::<u64, { CAP - 1 }, GlobalAllocator>(
46///             spmc::Options::global_mlocked(),
47///         );
48///         let _fail = new::<u64, { CAP + 1 }, GlobalAllocator>(
49///             spmc::Options::global_mlocked(),
50///         );
51///     }
52/// });
53/// ```
54pub fn new<T, const CAPACITY: usize, A>(
55    options: Options<A>,
56) -> (Producer<T, CAPACITY, A>, Consumer<T, CAPACITY, A>)
57where
58    T: bytemuck::AnyBitPattern,
59    A: Allocator,
60{
61    const {
62        assert!(
63            CAPACITY.is_power_of_two(),
64            "Given capacity is not a power of two",
65        );
66    }
67    let slots = A::allocate::<Slot<T>>(CAPACITY, options.alloc);
68    // Initialize every slot before any producer/consumer can touch it. The
69    // seqlock protocol assumes `seq` starts at an even value (0); without
70    // this loop, an allocator that returns garbage (e.g. GlobalAllocator on
71    // arenas with reused memory) can cause the first read to see a bogus
72    // matching `seq` and hand back uninitialized `T`.
73    for ix in 0..CAPACITY {
74        // SAFETY: `ix < CAPACITY`, so the derived pointer is inside the
75        // allocation returned by `A::allocate` and is aligned and
76        // dereferenceable per the `Allocator` contract.
77        let slot = unsafe { slots.ptr().wrapping_add(ix).as_mut_unchecked() };
78        // SAFETY: `slot.get()` returns a `*mut MaybeUninit<Slot<T>>` into
79        // uninitialized memory that no one else observes yet, so the write
80        // does not overlap any live reference.
81        unsafe {
82            slot.get().write(MaybeUninit::new(Slot {
83                seq: AtomicUsize::new(0),
84                data: UnsafeCell::new(T::zeroed()),
85            }));
86        }
87    }
88    let q = Arc::new(Queue {
89        producer_state: ProducerStateInner {
90            write_cursor: AtomicUsize::new(0),
91        }
92        .into(),
93        slots,
94    });
95    let producer = Producer::new(q.clone());
96    let consumer = Consumer::new(q);
97    (producer, consumer)
98}
99
100/// One cell in the SPMC ring. Carries the payload plus a sequence number the
101/// seqlock protocol uses to detect torn or lapped reads.
102///
103/// Exposed only so callers can name the [`Allocation<Slot<T>>`](Allocation)
104/// bound when writing generic helpers over the queue; there are no methods
105/// intended for direct use.
106pub struct Slot<T>
107where
108    T: bytemuck::AnyBitPattern,
109{
110    pub(super) seq: AtomicUsize,
111    pub(super) data: UnsafeCell<T>,
112}
113
114pub(super) struct Queue<T, const CAPACITY: usize, A>
115where
116    T: bytemuck::AnyBitPattern,
117    A: Allocator,
118{
119    pub(super) producer_state: ProducerState,
120    pub(super) slots: A::Allocation<Slot<T>>,
121}
122
123// SAFETY: Queue uses Slot<T> which is !Sync because of UnsafeCell, but the queue itself can only be
124// used through publish/Consumer APIs both of which synchronize themselves using seqlock seq.
125unsafe impl<T, const CAPACITY: usize, A> Sync for Queue<T, CAPACITY, A>
126where
127    T: bytemuck::AnyBitPattern,
128    A: Allocator,
129{
130}
131
132impl<T, const CAPACITY: usize, A> Queue<T, CAPACITY, A>
133where
134    T: bytemuck::AnyBitPattern,
135    A: Allocator,
136{
137    #[inline]
138    pub(super) fn publish(&self, value: T) {
139        let w_pos = self.producer_state.write_cursor.load(Ordering::Relaxed);
140        // Used as both generation control and even-odd guarantee
141        let seq_no = w_pos.wrapping_mul(2);
142        let slot = self.slot(w_pos);
143        slot.seq.store(seq_no.wrapping_add(1), Ordering::Relaxed);
144        // ARM: prevent the subsequent write from moving above the odd seq store
145        fence(Ordering::Release);
146        unsafe { slot.data.get().write_volatile(value) };
147        slot.seq.store(seq_no.wrapping_add(2), Ordering::Release);
148        self.producer_state
149            .write_cursor
150            .store(w_pos.wrapping_add(1), Ordering::Release);
151    }
152
153    /// Wraps the given `i` around `CAPACITY - 1`.
154    ///
155    /// # Safety
156    ///
157    /// - The masked index is always in `[0, CAPACITY)`, so the derived
158    ///   pointer stays inside the allocation.
159    /// - The allocation is aligned and dereferenceable per the `Allocator`
160    ///   contract.
161    /// - Every slot is initialized in [`new`] before the `Queue` is wrapped
162    ///   in `Arc`, so `assume_init_ref` is sound.
163    /// - Returning `&Slot<T>` while another thread mutates through
164    ///   `slot.data` / `slot.seq` is legal because the mutable state sits
165    ///   behind `UnsafeCell` and atomics.
166    #[inline(always)]
167    fn slot(&self, i: usize) -> &Slot<T> {
168        unsafe {
169            self.slots
170                .ptr()
171                .wrapping_add(i & (CAPACITY - 1))
172                .as_ref_unchecked()
173                .get()
174                .as_ref_unchecked()
175                .assume_init_ref()
176        }
177    }
178}
179
180#[cfg(test)]
181#[cfg(feature = "tests_basic")]
182mod tests {
183    use super::*;
184    #[cfg(not(feature = "tests_hugepage"))]
185    use crate::mem::global::GlobalAllocator;
186    #[cfg(feature = "tests_hugepage")]
187    use crate::mem::hugepages::{HugepageAllocator, HugepageAllocatorOptions, HugepageSize};
188    use crate::mem::test_util::NeverAllocator;
189    use crate::spmc::consumer::ReadResult;
190
191    static_assertions::assert_impl_all!(Queue<u32, 1, NeverAllocator>: Sync, Send);
192
193    #[cfg(not(feature = "tests_hugepage"))]
194    fn spmc_options() -> Options<GlobalAllocator> {
195        Options::global_mlocked()
196    }
197    #[cfg(feature = "tests_hugepage")]
198    fn spmc_options() -> Options<HugepageAllocator> {
199        Options::builder()
200            .alloc(
201                HugepageAllocatorOptions::builder()
202                    .mlock(true)
203                    .hugepage_size(HugepageSize::H2MB)
204                    .build(),
205            )
206            .build()
207    }
208
209    #[test]
210    fn single_thread_multiple_consumers_read_messages() {
211        const CAPACITY: usize = 128;
212        let (producer, mut c1) = new::<u64, CAPACITY, _>(spmc_options());
213        let mut c2 = c1.clone();
214        let mut c3 = c1.clone();
215        assert_eq!(c1.try_read(), ReadResult::Empty);
216        assert_eq!(c2.try_read(), ReadResult::Empty);
217        assert_eq!(c3.try_read(), ReadResult::Empty);
218
219        producer.publish(1);
220        assert_eq!(c1.try_read(), ReadResult::Value(1));
221        assert_eq!(c2.try_read(), ReadResult::Value(1));
222        assert_eq!(c3.try_read(), ReadResult::Value(1));
223        assert_eq!(c1.try_read(), ReadResult::Empty);
224        assert_eq!(c2.try_read(), ReadResult::Empty);
225        assert_eq!(c3.try_read(), ReadResult::Empty);
226
227        producer.publish(2);
228        assert_eq!(c1.try_read(), ReadResult::Value(2));
229        assert_eq!(c2.try_read(), ReadResult::Value(2));
230        assert_eq!(c1.try_read(), ReadResult::Empty);
231        assert_eq!(c2.try_read(), ReadResult::Empty);
232
233        producer.publish(3);
234        assert_eq!(c1.try_read(), ReadResult::Value(3));
235        assert_eq!(c1.try_read(), ReadResult::Empty);
236
237        assert_eq!(c2.try_read(), ReadResult::Value(3));
238        assert_eq!(c2.try_read(), ReadResult::Empty);
239
240        assert_eq!(c3.try_read(), ReadResult::Value(2));
241        assert_eq!(c3.try_read(), ReadResult::Value(3));
242        assert_eq!(c3.try_read(), ReadResult::Empty);
243    }
244
245    #[test]
246    fn concurrent_single_producer_multiple_consumers() {
247        use std::sync::Arc;
248        use std::sync::atomic::{AtomicBool, Ordering};
249        use std::thread;
250
251        const CAPACITY: usize = 1024;
252        const NCONSUMERS: usize = 4;
253        const N: u64 = 200_000;
254
255        let (producer, c0) = new::<u64, CAPACITY, _>(spmc_options());
256        let consumers: Vec<_> = (0..NCONSUMERS).map(|_| c0.clone()).collect();
257        drop(c0);
258        let done = Arc::new(AtomicBool::new(false));
259
260        let handles: Vec<_> = consumers
261            .into_iter()
262            .map(|mut c| {
263                let done = done.clone();
264                thread::spawn(move || -> u64 {
265                    let mut last: Option<u64> = None;
266                    let mut got = 0u64;
267                    loop {
268                        match c.try_read() {
269                            ReadResult::Value(v) => {
270                                if let Some(prev) = last {
271                                    assert!(v > prev);
272                                }
273                                last = Some(v);
274                                got += 1;
275                            }
276                            ReadResult::Lapped { .. } => {}
277                            ReadResult::Empty => {
278                                if done.load(Ordering::Acquire) {
279                                    while let ReadResult::Value(v) = c.try_read() {
280                                        if let Some(prev) = last {
281                                            assert!(v > prev);
282                                        }
283                                        last = Some(v);
284                                        got += 1;
285                                    }
286                                    return got;
287                                }
288                            }
289                        }
290                    }
291                })
292            })
293            .collect();
294
295        for i in 0..N {
296            producer.publish(i);
297        }
298        done.store(true, Ordering::Release);
299
300        for h in handles {
301            let got = h.join().unwrap();
302            assert!(got > 0);
303        }
304    }
305
306    #[test]
307    fn consumer_can_be_overlapped_by_writer() {
308        const CAPACITY: usize = 8;
309        let (producer, mut c1) = new::<usize, CAPACITY, _>(spmc_options());
310        assert_eq!(c1.try_read(), ReadResult::Empty);
311
312        // After this loop write_cursor = 8. First try_read below loads it into
313        // cached_write_cursor and returns the first item.
314        for i in 0..CAPACITY {
315            producer.publish(i);
316        }
317        // read_cursor becomes 1.
318        assert_eq!(c1.try_read(), ReadResult::Value(0));
319        // Two more publishes lap the consumer. write_cursor = 10 after these.
320        producer.publish(CAPACITY + 1);
321        producer.publish(CAPACITY + 2);
322        // The Lapped branch reloads cached_write_cursor (= 10) and jumps to
323        // cached - 1 = 9. skipped = 9 - read_cursor = 9 - 1 = 8.
324        assert_eq!(c1.try_read(), ReadResult::Lapped { skipped: 8 });
325        // Slot 9 & 7 = slot 1, last written at w_pos = 9 with data = CAPACITY + 2.
326        assert_eq!(c1.try_read(), ReadResult::Value(CAPACITY + 2));
327    }
328}
329
330#[cfg(test)]
331#[cfg(feature = "tests_dhat")]
332mod tests_dhat {
333    use super::*;
334    use crate::mem::global::GlobalAllocator;
335
336    #[test]
337    fn hot_path_zero_allocations() {
338        let _profiler = dhat::Profiler::builder().testing().build();
339        let (producer, mut c1) = new::<u64, 1024, GlobalAllocator>(Options::global_mlocked());
340        let mut c2 = c1.clone();
341        let mut c3 = c1.clone();
342
343        // Warm up to absorb any one-time platform allocations: lazy symbol
344        // resolution in the dynamic linker, libstd TLS init, debug-build
345        // slow-path stubs that LLVM emits. None of these scale with iteration
346        // count; the per-iteration regression we actually care about would
347        // dwarf the slack budget below.
348        for i in 0..1024u64 {
349            let _ = producer.publish(i);
350        }
351        for _ in 0..1024 {
352            let _ = c1.try_read();
353            let _ = c2.try_read();
354            let _ = c3.try_read();
355        }
356        let _ = dhat::HeapStats::get();
357
358        let before = dhat::HeapStats::get();
359
360        for i in 0..1024u64 {
361            let _ = producer.publish(i);
362        }
363        for _ in 0..1024 {
364            let _ = c1.try_read();
365            let _ = c2.try_read();
366            let _ = c3.try_read();
367        }
368
369        let after = dhat::HeapStats::get();
370        let allocs = after.total_blocks - before.total_blocks;
371        dhat::assert!(
372            allocs == 0,
373            "hot path allocated {} blocks; a real regression would be O(iters)",
374            allocs,
375        );
376    }
377}