Skip to main content

aoc/util/
thread.rs

1//! Utility methods to spawn a number of
2//! [scoped](https://doc.rust-lang.org/stable/std/thread/fn.scope.html)
3//! threads equal to the number of cores on the machine. Unlike normal threads, scoped threads
4//! can borrow data from their environment.
5use std::iter::repeat_with;
6use std::sync::atomic::Ordering::Relaxed;
7use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize};
8use std::thread::*;
9
10pub struct ParIter<'a, T> {
11    id: usize,
12    items: &'a [T],
13    workers: &'a [CachePadding],
14}
15
16impl<'a, T> Iterator for ParIter<'a, T> {
17    type Item = &'a T;
18
19    fn next(&mut self) -> Option<&'a T> {
20        // First try taking from our own queue.
21        let worker = &self.workers[self.id];
22        let current = worker.increment();
23        let (start, end) = unpack(current);
24
25        // There are still items to process.
26        if start < end {
27            return Some(&self.items[start]);
28        }
29
30        // Steal from another worker, [spinlocking](https://en.wikipedia.org/wiki/Spinlock)
31        // until we acquire new items to process or there's nothing left to do.
32        loop {
33            // Find worker with the most remaining items, breaking out of the loop
34            // and returning `None` if there is no work remaining.
35            let (other, current, size) = self
36                .workers
37                .iter()
38                .filter_map(|other| {
39                    let current = other.load();
40                    let (start, end) = unpack(current);
41                    let size = end.saturating_sub(start);
42
43                    (size > 0).then_some((other, current, size))
44                })
45                .max_by_key(|&(_, _, size)| size)?;
46
47            // Split the work items into two roughly equal piles.
48            let (start, end) = unpack(current);
49            let middle = start + size.div_ceil(2);
50
51            let next = pack(middle, end);
52            let stolen = pack(start + 1, middle);
53
54            // We could be preempted by another thread stealing or by the owning worker
55            // thread finishing an item, so check indices are still unmodified.
56            if other.compare_exchange(current, next) {
57                worker.store(stolen);
58                break Some(&self.items[start]);
59            }
60        }
61    }
62}
63
64/// Intentionally force alignment to 128 bytes to make a best effort attempt to place each atomic
65/// on its own cache line. This reduces contention and improves performance for common
66/// CPU caching protocols such as [MESI](https://en.wikipedia.org/wiki/MESI_protocol).
67#[repr(align(128))]
68pub struct CachePadding {
69    atomic: AtomicUsize,
70}
71
72/// Convenience wrapper methods around atomic operations. Both start and end indices are packed
73/// into a single atomic so that we can use the fastest and easiest to reason about `Relaxed`
74/// ordering.
75impl CachePadding {
76    #[inline]
77    fn new(n: usize) -> Self {
78        Self { atomic: AtomicUsize::new(n) }
79    }
80
81    #[inline]
82    fn increment(&self) -> usize {
83        self.atomic.fetch_add(1, Relaxed)
84    }
85
86    #[inline]
87    fn load(&self) -> usize {
88        self.atomic.load(Relaxed)
89    }
90
91    #[inline]
92    fn store(&self, n: usize) {
93        self.atomic.store(n, Relaxed);
94    }
95
96    #[inline]
97    fn compare_exchange(&self, current: usize, new: usize) -> bool {
98        self.atomic.compare_exchange(current, new, Relaxed, Relaxed).is_ok()
99    }
100}
101
102/// Shares a monotonically increasing value between multiple threads.
103pub struct AtomicIter {
104    running: AtomicBool,
105    index: AtomicU32,
106    step: u32,
107}
108
109impl AtomicIter {
110    pub fn new(start: u32, step: u32) -> Self {
111        Self { running: AtomicBool::new(true), index: AtomicU32::from(start), step }
112    }
113
114    pub fn next(&self) -> Option<u32> {
115        self.running.load(Relaxed).then(|| self.index.fetch_add(self.step, Relaxed))
116    }
117
118    pub fn stop(&self) {
119        self.running.store(false, Relaxed);
120    }
121}
122
123/// Usually the number of physical cores.
124pub fn threads() -> usize {
125    available_parallelism().unwrap().get()
126}
127
128/// Spawn `n` scoped threads, where `n` is the available parallelism.
129pub fn spawn<F, R>(f: F) -> Vec<R>
130where
131    F: Fn() -> R + Copy + Send,
132    R: Send,
133{
134    scope(|scope| {
135        let handles: Vec<_> = repeat_with(|| scope.spawn(f)).take(threads()).collect();
136        handles.into_iter().flat_map(ScopedJoinHandle::join).collect()
137    })
138}
139
140/// Spawns `n` scoped threads that each receive a
141/// [work stealing](https://en.wikipedia.org/wiki/Work_stealing) iterator.
142/// Work stealing is an efficient strategy that keeps each CPU core busy when some items take longer
143/// than others to process, used by popular libraries such as [rayon](https://github.com/rayon-rs/rayon).
144/// Processing at different rates also happens on many modern CPUs with
145/// [heterogeneous performance and efficiency cores](https://en.wikipedia.org/wiki/ARM_big.LITTLE).
146pub fn spawn_parallel_iterator<F, R, T>(items: &[T], f: F) -> Vec<R>
147where
148    F: Fn(ParIter<'_, T>) -> R + Copy + Send,
149    R: Send,
150    T: Sync,
151{
152    let threads = threads();
153    let size = items.len().div_ceil(threads);
154
155    // Initially divide work as evenly as possible among the worker threads.
156    let workers: Vec<_> = (0..threads)
157        .map(|id| {
158            let start = (id * size).min(items.len());
159            let end = (start + size).min(items.len());
160            CachePadding::new(pack(start, end))
161        })
162        .collect();
163    let workers = workers.as_slice();
164
165    scope(|scope| {
166        let handles: Vec<_> =
167            (0..threads).map(|id| scope.spawn(move || f(ParIter { id, items, workers }))).collect();
168        handles.into_iter().flat_map(ScopedJoinHandle::join).collect()
169    })
170}
171
172#[inline]
173fn pack(start: usize, end: usize) -> usize {
174    (end << 32) | start
175}
176
177#[inline]
178fn unpack(both: usize) -> (usize, usize) {
179    (both & 0xffffffff, both >> 32)
180}