Skip to main content

kime_cpu/
par.rs

1//! Splitting work across threads.
2//!
3//! This is the plain version the reference kernels use: scoped threads per call and a shared
4//! counter that hands out tasks in order. The pinned worker groups from spec/10-cpu.md replace it
5//! when the plan executor lands. Nothing a task computes may depend on which thread runs it, which
6//! is what keeps the kernels deterministic under any thread count.
7
8use std::marker::PhantomData;
9use std::num::NonZero;
10use std::sync::Mutex;
11use std::sync::atomic::{AtomicUsize, Ordering};
12
13/// The number of threads the machine offers.
14#[must_use]
15pub fn available() -> usize {
16    std::thread::available_parallelism().map_or(1, NonZero::get)
17}
18
19/// Runs `f(i)` for every `i` in `0..n` on up to `threads` threads.
20pub fn for_each(n: usize, threads: usize, f: impl Fn(usize) + Sync) {
21    let threads = threads.clamp(1, n.max(1));
22    if threads == 1 {
23        (0..n).for_each(f);
24        return;
25    }
26    let next = AtomicUsize::new(0);
27    let work = || {
28        loop {
29            let i = next.fetch_add(1, Ordering::Relaxed);
30            if i >= n {
31                break;
32            }
33            f(i);
34        }
35    };
36    std::thread::scope(|s| {
37        for _ in 1..threads {
38            s.spawn(work);
39        }
40        work();
41    });
42}
43
44/// `(0..n).map(f).collect()` on up to `threads` threads, in order.
45///
46/// # Panics
47///
48/// If `f` panics.
49pub fn map<T: Send>(n: usize, threads: usize, f: impl Fn(usize) -> T + Sync) -> Vec<T> {
50    let slots: Vec<Mutex<Option<T>>> = (0..n).map(|_| Mutex::new(None)).collect();
51    for_each(n, threads, |i| *slots[i].lock().unwrap() = Some(f(i)));
52    slots.into_iter().map(|s| s.into_inner().unwrap().unwrap()).collect()
53}
54
55/// A buffer many threads write into at once, each to elements no other thread touches.
56#[derive(Debug)]
57pub struct Shared<'a> {
58    ptr: *mut f32,
59    len: usize,
60    _borrow: PhantomData<&'a mut [f32]>,
61}
62
63// SAFETY: Shared is a &mut [f32] split across threads. The only access is `set`, whose contract
64// makes every element written by at most one thread, so sending and sharing it is sound.
65unsafe impl Send for Shared<'_> {}
66// SAFETY: as above.
67unsafe impl Sync for Shared<'_> {}
68
69impl<'a> Shared<'a> {
70    /// Borrows `buf` for the lifetime of the Shared.
71    pub fn new(buf: &'a mut [f32]) -> Self {
72        Self { ptr: buf.as_mut_ptr(), len: buf.len(), _borrow: PhantomData }
73    }
74
75    /// Writes `v` at `i`.
76    ///
77    /// # Safety
78    ///
79    /// No other thread may read or write element `i` while this Shared is alive.
80    ///
81    /// # Panics
82    ///
83    /// If `i` is out of bounds.
84    #[inline(always)]
85    pub unsafe fn set(&self, i: usize, v: f32) {
86        assert!(i < self.len);
87        // SAFETY: in bounds by the assert, and the caller guarantees no other thread touches i.
88        unsafe { self.ptr.add(i).write(v) }
89    }
90}
91
92impl Shared<'_> {
93    /// Elements `start..start + len` as a slice.
94    ///
95    /// # Safety
96    ///
97    /// No other thread may read or write those elements while the slice lives.
98    ///
99    /// # Panics
100    ///
101    /// If the range is out of bounds.
102    #[inline(always)]
103    #[allow(clippy::mut_from_ref)]
104    pub unsafe fn slice_mut(&self, start: usize, len: usize) -> &mut [f32] {
105        assert!(start + len <= self.len);
106        // SAFETY: in bounds by the assert, and the caller guarantees no other thread touches it.
107        unsafe { std::slice::from_raw_parts_mut(self.ptr.add(start), len) }
108    }
109
110    /// Reads element `i`.
111    ///
112    /// # Safety
113    ///
114    /// No other thread may write element `i` while this Shared is alive.
115    ///
116    /// # Panics
117    ///
118    /// If `i` is out of bounds.
119    #[inline(always)]
120    pub unsafe fn get(&self, i: usize) -> f32 {
121        assert!(i < self.len);
122        // SAFETY: in bounds by the assert, and the caller guarantees no other thread writes i.
123        unsafe { self.ptr.add(i).read() }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn every_task_runs_once() {
133        for threads in [1, 2, 7] {
134            for n in [0, 1, 5, 100] {
135                let got = map(n, threads, |i| i * 2);
136                assert_eq!(got, (0..n).map(|i| i * 2).collect::<Vec<_>>());
137            }
138        }
139    }
140}