Skip to main content

low_latency_data_structures/spmc/
queue.rs

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