Skip to main content

low_latency_data_structures/spsc/
queue.rs

1use crossbeam_utils::CachePadded;
2
3use crate::mem::{Allocation, Allocator};
4use crate::shim::sync::atomic::AtomicUsize;
5use crate::shim::sync::{Arc, atomic};
6use crate::spsc::builder::Options;
7use crate::spsc::consumer::Consumer;
8use crate::spsc::producer::Producer;
9
10/// Creates a new SPSC queue with `CAPACITY` slots using the specified allocator.
11///
12/// `CAPACITY` must be a power of two (compile-time enforced). The slot
13/// buffer is allocated up front and locked into RAM via `mlock` so the
14/// hot path never page-faults.
15///
16/// # Examples
17///
18/// ```
19/// use low_latency_data_structures::spsc::{self, new};
20/// use low_latency_data_structures::mem::global::GlobalAllocator;
21///
22/// let (mut producer, mut consumer) = new::<u64, 8, GlobalAllocator>(
23///     spsc::Options::global_mlocked(),
24/// );
25/// assert_eq!(producer.push(1), None);
26/// assert_eq!(consumer.pop(), Some(1));
27/// ```
28///
29/// Capacities that are not powers of two fail to compile:
30///
31/// ```compile_fail
32/// use low_latency_data_structures::spsc::{self, new};
33/// use low_latency_data_structures::mem::global::GlobalAllocator;
34/// # use seq_macro::seq;
35/// seq!(N in 2..20 {
36///     {
37///         const CAP: usize = 2usize.wrapping_pow(N);
38///         let _fail = new::<u64, { CAP - 1 }, GlobalAllocator>(
39///             spsc::Options::global_mlocked(),
40///         );
41///         let _fail = new::<u64, { CAP + 1 }, GlobalAllocator>(
42///             spsc::Options::global_mlocked(),
43///         );
44///     }
45/// });
46/// ```
47pub fn new<T, const CAPACITY: usize, A>(
48    options: Options<A>,
49) -> (Producer<T, CAPACITY, A>, Consumer<T, CAPACITY, A>)
50where
51    A: Allocator,
52{
53    const {
54        assert!(
55            CAPACITY.is_power_of_two(),
56            "Given capacity is not a power of two!"
57        );
58    };
59    let slots_allocation = A::allocate(CAPACITY, options.alloc);
60    let q = Arc::new(Queue {
61        head: AtomicUsize::new(0).into(),
62        tail: AtomicUsize::new(0).into(),
63        slots_allocation,
64    });
65    let producer = Producer::new(q.clone());
66    let consumer = Consumer::new(q);
67    (producer, consumer)
68}
69
70pub(super) struct Queue<T, const CAPACITY: usize, A>
71where
72    A: Allocator,
73{
74    pub(super) slots_allocation: A::Allocation<T>,
75    pub(super) head: CachePadded<AtomicUsize>,
76    pub(super) tail: CachePadded<AtomicUsize>,
77}
78
79impl<T, const CAPACITY: usize, A> Drop for Queue<T, CAPACITY, A>
80where
81    A: Allocator,
82{
83    fn drop(&mut self) {
84        let head = self.head.load(atomic::Ordering::Relaxed);
85        let tail = self.tail.load(atomic::Ordering::Relaxed);
86        let count = tail.wrapping_sub(head);
87        for k in 0..count {
88            let i = head.wrapping_add(k);
89            let slot_ptr = self.slots_allocation.ptr().wrapping_add(i & (CAPACITY - 1));
90            // SAFETY: it's not null because `i & (sef.capacity - 1)` limits it to [0;
91            // allocated_cap). And it can be safely converted to a reference because T is self
92            // contained bitwise.
93            unsafe {
94                slot_ptr.as_ref_unchecked().with_mut(|ptr| {
95                    ptr.as_mut_unchecked().assume_init_drop();
96                })
97            }
98        }
99    }
100}
101
102#[cfg(test)]
103#[cfg(feature = "tests_basic")]
104mod tests_basic {
105    use std::cell::Cell;
106    use std::rc::Rc;
107    use std::thread;
108
109    use seq_macro::seq;
110
111    use super::new;
112    #[cfg(not(feature = "tests_hugepage"))]
113    use crate::mem::global::{GlobalAllocator, GlobalAllocatorOptions};
114    #[cfg(feature = "tests_hugepage")]
115    use crate::mem::hugepages::{HugepageAllocator, HugepageAllocatorOptions, HugepageSize};
116    use crate::spsc::builder::Options;
117
118    #[cfg(not(feature = "tests_hugepage"))]
119    fn alloc_options() -> GlobalAllocatorOptions {
120        GlobalAllocatorOptions::builder().mlock(true).build()
121    }
122
123    #[cfg(not(feature = "tests_hugepage"))]
124    type Alloc = GlobalAllocator;
125    #[cfg(feature = "tests_hugepage")]
126    type Alloc = HugepageAllocator;
127
128    #[cfg(feature = "tests_hugepage")]
129    fn alloc_options() -> HugepageAllocatorOptions {
130        HugepageAllocatorOptions::builder()
131            .mlock(true)
132            .hugepage_size(HugepageSize::H2MB)
133            .build()
134    }
135
136    fn spsc_options() -> Options<Alloc> {
137        Options::builder().alloc(alloc_options()).build()
138    }
139
140    #[test]
141    fn move_producer_consumer_to_threads() {
142        let (mut producer, mut consumer) = new::<_, 2, Alloc>(spsc_options());
143        thread::spawn(move || {
144            let _ = producer.push(123);
145        })
146        .join()
147        .unwrap();
148        thread::spawn(move || {
149            assert_eq!(consumer.pop(), Some(123));
150        })
151        .join()
152        .unwrap();
153    }
154
155    #[test]
156    fn handoff_one_value() {
157        let (mut producer, mut consumer) = new::<_, 2, Alloc>(spsc_options());
158        assert_eq!(producer.push(123), None);
159        assert_eq!(consumer.pop(), Some(123));
160    }
161
162    #[test]
163    fn allows_queues_with_powers_of_two_capacity() {
164        #[cfg(feature = "tests_hugepage")]
165        seq!(N in 0..6 {
166            {
167                const POWER: usize = 2usize.pow(N);
168                new::<u32, POWER, Alloc>(spsc_options());
169            }
170        });
171        #[cfg(not(feature = "tests_hugepage"))]
172        seq!(N in 0..20 {
173            {
174                const POWER: usize = 2usize.pow(N);
175                new::<u32, POWER, Alloc>(spsc_options());
176            }
177        });
178    }
179
180    #[test]
181    fn drops_unread_items() {
182        let counter = Rc::new(Cell::new(0));
183        #[derive(Debug, PartialEq)]
184        struct Droppable {
185            counter: Rc<Cell<usize>>,
186        }
187        impl Drop for Droppable {
188            fn drop(&mut self) {
189                let cnt = self.counter.get();
190                self.counter.set(cnt + 1);
191            }
192        }
193        const CAPACITY: usize = 64;
194        let (mut producer, mut consumer) = new::<_, CAPACITY, Alloc>(spsc_options());
195        for _ in 0..CAPACITY {
196            let counter = counter.clone();
197            assert_eq!(producer.push(Droppable { counter }), None);
198        }
199        let read = CAPACITY / 2;
200        for _ in 0..read {
201            assert!(matches!(consumer.pop(), Some(_)));
202        }
203        assert_eq!(read, counter.get());
204        drop(producer);
205        drop(consumer);
206        assert_eq!(CAPACITY, counter.get());
207    }
208
209    #[test]
210    fn empty_returns_none() {
211        let (_, mut consumer) = new::<i32, 4, Alloc>(spsc_options());
212        assert_eq!(consumer.pop(), None);
213        assert_eq!(consumer.pop(), None);
214    }
215
216    #[test]
217    fn full_returns_item_back() {
218        let (mut producer, _) = new::<i32, 2, Alloc>(spsc_options());
219        assert_eq!(producer.push(1), None);
220        assert_eq!(producer.push(2), None);
221        // Full: item comes back untouched
222        assert_eq!(producer.push(3), Some(3));
223        assert_eq!(producer.push(4), Some(4));
224    }
225
226    #[test]
227    fn fifo_ordering() -> anyhow::Result<()> {
228        let (mut producer, mut consumer) = new::<_, 8, Alloc>(spsc_options());
229        for i in 0..8 {
230            assert_eq!(producer.push(i), None);
231        }
232        for i in 0..8 {
233            assert_eq!(consumer.pop(), Some(i));
234        }
235        Ok(())
236    }
237
238    #[test]
239    fn wraparound_n_laps() {
240        const CAPACITY: usize = 4;
241        let laps = 100;
242        let (mut producer, mut consumer) = new::<_, CAPACITY, Alloc>(spsc_options());
243        for lap in 0..laps {
244            for i in 0..CAPACITY {
245                let val = lap * CAPACITY + i;
246                assert_eq!(producer.push(val), None, "push failed at lap {lap}, i {i}");
247            }
248            // Queue is full
249            assert_eq!(producer.push(9999), Some(9999));
250            for i in 0..CAPACITY {
251                let val = lap * CAPACITY + i;
252                assert_eq!(consumer.pop(), Some(val), "wrong value at lap {lap}, i {i}");
253            }
254            // Queue is empty
255            assert_eq!(consumer.pop(), None);
256        }
257    }
258
259    #[test]
260    fn interleaved_push_pop() {
261        let (mut producer, mut consumer) = new::<_, 2, Alloc>(spsc_options());
262        // Push 1, pop 1, repeat: tests wraparound with tiny queue
263        for i in 0..1000 {
264            assert_eq!(producer.push(i), None);
265            assert_eq!(consumer.pop(), Some(i));
266        }
267    }
268
269    #[test]
270    fn capacity_one() {
271        let (mut producer, mut consumer) = new::<_, 1, Alloc>(spsc_options());
272        assert_eq!(consumer.pop(), None);
273        assert_eq!(producer.push(42), None);
274        assert_eq!(producer.push(43), Some(43)); // full
275        assert_eq!(consumer.pop(), Some(42));
276        assert_eq!(consumer.pop(), None); // empty again
277    }
278
279    #[test]
280    fn move_only_type() {
281        // Verify non-Copy, non-Clone types work
282        let (mut producer, mut consumer) = new::<_, 4, Alloc>(spsc_options());
283        let s = String::from("hello");
284        assert_eq!(producer.push(s), None);
285        let got = consumer.pop().unwrap();
286        assert_eq!(got, "hello");
287    }
288}
289
290#[cfg(test)]
291#[cfg(feature = "tests_loom")]
292mod tests_loom {
293    use super::*;
294    use crate::mem::loom::{LoomVecAllocator, LoomVecAllocatorOptions};
295
296    static_assertions::assert_cfg!(
297        not(feature = "tests_hugepage"),
298        "tests_loom incompatible with tests_hugepage as loom uses custom buffer allocator",
299    );
300
301    fn spsc_options() -> Options<LoomVecAllocator> {
302        Options::builder().alloc(LoomVecAllocatorOptions).build()
303    }
304
305    #[test]
306    fn concurrent_push_pop() {
307        loom::model(|| {
308            let (mut producer, mut consumer) = new::<i32, 4, _>(spsc_options());
309
310            let t1 = loom::thread::spawn(move || {
311                let _ = producer.push(1);
312                let _ = producer.push(2);
313                let _ = producer.push(3);
314            });
315
316            let t2 = loom::thread::spawn(move || {
317                let mut collected = Vec::new();
318                while collected.len() < 3 {
319                    if let Some(v) = consumer.pop() {
320                        collected.push(v);
321                    } else {
322                        loom::thread::yield_now();
323                    }
324                }
325                collected
326            });
327
328            t1.join().unwrap();
329            let values = t2.join().unwrap();
330            // FIFO: must be in order
331            assert_eq!(values, vec![1, 2, 3]);
332        });
333    }
334
335    /// Producer must retry if the queue is full.
336    #[test]
337    fn concurrent_with_full_queue() {
338        loom::model(|| {
339            let (mut producer, mut consumer) = new::<i32, 1, _>(spsc_options());
340
341            let t1 = loom::thread::spawn(move || {
342                for i in 0..3 {
343                    while producer.push(i).is_some() {
344                        loom::thread::yield_now();
345                    }
346                }
347            });
348
349            let t2 = loom::thread::spawn(move || {
350                let mut collected = Vec::new();
351                while collected.len() < 3 {
352                    if let Some(v) = consumer.pop() {
353                        collected.push(v);
354                    } else {
355                        loom::thread::yield_now();
356                    }
357                }
358                collected
359            });
360
361            t1.join().unwrap();
362            let values = t2.join().unwrap();
363            assert_eq!(values, vec![0, 1, 2]);
364        });
365    }
366
367    #[test]
368    fn concurrent_no_values_lost() {
369        loom::model(|| {
370            let (mut producer, mut consumer) = new::<i32, 2, _>(spsc_options());
371
372            let t1 = loom::thread::spawn(move || {
373                for i in 0..3 {
374                    while producer.push(i).is_some() {
375                        loom::thread::yield_now();
376                    }
377                }
378            });
379
380            let t2 = loom::thread::spawn(move || {
381                let mut sum = 0;
382                let mut count = 0;
383                while count < 3 {
384                    if let Some(v) = consumer.pop() {
385                        sum += v;
386                        count += 1;
387                    } else {
388                        loom::thread::yield_now();
389                    }
390                }
391                (count, sum)
392            });
393
394            t1.join().unwrap();
395            let (count, sum) = t2.join().unwrap();
396            assert_eq!(count, 3);
397            assert_eq!(sum, 0 + 1 + 2);
398        });
399    }
400}
401
402#[cfg(test)]
403#[cfg(feature = "tests_dhat")]
404mod tests_dhat {
405    use super::*;
406    use crate::mem::global::GlobalAllocator;
407
408    #[test]
409    fn hot_path_zero_allocations() {
410        let _profiler = dhat::Profiler::builder().testing().build();
411        let (mut producer, mut consumer) =
412            new::<u64, 1024, GlobalAllocator>(Options::global_mlocked());
413
414        // Warm up to absorb any one-time platform allocations: lazy symbol
415        // resolution in the dynamic linker, libstd TLS init, debug-build
416        // slow-path stubs that LLVM emits. None of these scale with iteration
417        // count; the per-iteration regression we actually care about would
418        // dwarf the slack budget below.
419        for i in 0..1024u64 {
420            let _ = producer.push(i);
421        }
422        for _ in 0..1024 {
423            let _ = consumer.pop();
424        }
425        let _ = dhat::HeapStats::get();
426
427        let before = dhat::HeapStats::get();
428
429        for i in 0..1024u64 {
430            let _ = producer.push(i);
431        }
432        for _ in 0..1024 {
433            let _ = consumer.pop();
434        }
435
436        let after = dhat::HeapStats::get();
437        let allocs = after.total_blocks - before.total_blocks;
438        dhat::assert!(
439            allocs == 0,
440            "hot path allocated {} blocks; a real regression would be O(iters)",
441            allocs,
442        );
443    }
444}