low_latency_data_structures/spmc/
queue.rs1use 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
12pub 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 for ix in 0..CAPACITY {
75 let slot = unsafe { slots.ptr().wrapping_add(ix).as_mut_unchecked() };
79 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
101pub 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
121unsafe 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 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 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 #[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 for i in 0..CAPACITY {
311 producer.publish(i);
312 }
313 assert_eq!(c1.try_read(), ReadResult::Value(0));
315 producer.publish(CAPACITY + 1);
317 producer.publish(CAPACITY + 2);
318 assert_eq!(c1.try_read(), ReadResult::Lapped { skipped: 8 });
321 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 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}