Skip to main content

axiolid_backend_cpu/
execution.rs

1//! Runtime CPU execution context. Operation providers may compose this type.
2
3use core::fmt;
4use std::num::NonZeroUsize;
5#[cfg(feature = "parallel")]
6use std::sync::{Arc, OnceLock};
7
8use crate::{CpuConfigError, CpuFeatures, CpuInstructionSet};
9
10/// CPU execution context with runtime ISA detection and, with `parallel`, a
11/// local pool of exactly [`Self::thread_count`] workers.
12///
13/// This type deliberately implements no geometry operation trait. A provider
14/// only implements `MeshBoolean`, `MeshCompiler`, or another capability
15/// after the algorithm exists and is verified.
16#[derive(Clone)]
17pub struct CpuExecution {
18    instruction_set: CpuInstructionSet,
19    features: CpuFeatures,
20    threads: NonZeroUsize,
21    /// The local pool, shared by clones. Started with the context by
22    /// [`crate::CpuExecutionBuilder`]; for [`Self::portable`] and
23    /// [`Self::detect`], on first use.
24    #[cfg(feature = "parallel")]
25    pool: Arc<OnceLock<rayon::ThreadPool>>,
26}
27
28impl fmt::Debug for CpuExecution {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        f.debug_struct("CpuExecution")
31            .field("instruction_set", &self.instruction_set)
32            .field("features", &self.features)
33            .field("threads", &self.threads)
34            .finish_non_exhaustive()
35    }
36}
37
38impl CpuExecution {
39    /// Always-available portable execution context.
40    pub fn portable() -> Self {
41        Self {
42            instruction_set: CpuInstructionSet::Portable,
43            features: CpuFeatures::detect(),
44            threads: NonZeroUsize::MIN,
45            #[cfg(feature = "parallel")]
46            pool: Arc::default(),
47        }
48    }
49
50    /// Best instruction path compiled into this binary and available at runtime.
51    pub fn detect() -> Self {
52        let features = CpuFeatures::detect();
53        #[cfg(feature = "simd")]
54        let instruction_set = features.best();
55        #[cfg(not(feature = "simd"))]
56        let instruction_set = CpuInstructionSet::Portable;
57        Self {
58            instruction_set,
59            features,
60            threads: NonZeroUsize::MIN,
61            #[cfg(feature = "parallel")]
62            pool: Arc::default(),
63        }
64    }
65
66    pub(crate) fn from_configuration(
67        instruction_set: CpuInstructionSet,
68        features: CpuFeatures,
69        threads: NonZeroUsize,
70    ) -> Result<Self, CpuConfigError> {
71        #[cfg(not(feature = "parallel"))]
72        if threads.get() > 1 {
73            return Err(CpuConfigError::ParallelFeatureDisabled);
74        }
75
76        // One worker gets a pool too: without one, `install` would run on
77        // whatever pool the caller is in, and a provider sizing its scratch
78        // or its work by `rayon::current_num_threads` would see that width
79        // instead of the one configured (#259).
80        #[cfg(feature = "parallel")]
81        let pool =
82            Arc::new(OnceLock::from(local_pool(threads).map_err(|error| {
83                CpuConfigError::ThreadPool(error.to_string())
84            })?));
85
86        Ok(Self {
87            instruction_set,
88            features,
89            threads,
90            #[cfg(feature = "parallel")]
91            pool,
92        })
93    }
94
95    /// Runtime-selected instruction set.
96    pub const fn instruction_set(&self) -> CpuInstructionSet {
97        self.instruction_set
98    }
99
100    /// Detected machine features.
101    pub const fn features(&self) -> CpuFeatures {
102        self.features
103    }
104
105    /// Configured worker bound.
106    pub const fn thread_count(&self) -> NonZeroUsize {
107        self.threads
108    }
109
110    /// Execute one closure inside this context's local Rayon pool, which has
111    /// exactly [`Self::thread_count`] workers, one included: inside it
112    /// `rayon::current_num_threads()` is that count, whatever pool the
113    /// caller runs in.
114    ///
115    /// # Panics
116    ///
117    /// For a context from [`Self::portable`] or [`Self::detect`], the pool
118    /// is started on first use, and this panics if the operating system
119    /// refuses the thread, as a first use of Rayon's global pool does. A
120    /// context from [`crate::CpuExecutionBuilder`] started its pool when it
121    /// was built and reported a failure there.
122    #[cfg(feature = "parallel")]
123    pub fn install<R: Send>(&self, operation: impl FnOnce() -> R + Send) -> R {
124        self.pool
125            .get_or_init(|| local_pool(self.threads).expect("start the local pool"))
126            .install(operation)
127    }
128}
129
130/// A pool of exactly `threads` workers.
131#[cfg(feature = "parallel")]
132fn local_pool(threads: NonZeroUsize) -> Result<rayon::ThreadPool, rayon::ThreadPoolBuildError> {
133    rayon::ThreadPoolBuilder::new()
134        .num_threads(threads.get())
135        .thread_name(|index| format!("axiolid-cpu-{index}"))
136        .build()
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::{CpuExecutionBuilder, InstructionPolicy};
143
144    fn assert_runtime_traits<T: fmt::Debug + Clone + Send + Sync>() {}
145
146    #[test]
147    fn portable_context_is_constructible_but_claims_no_operation_trait() {
148        assert_runtime_traits::<CpuExecution>();
149        let backend = CpuExecutionBuilder::new()
150            .instruction_policy(InstructionPolicy::Portable)
151            .build()
152            .expect("portable context");
153        assert_eq!(backend.instruction_set(), CpuInstructionSet::Portable);
154    }
155
156    #[cfg(not(feature = "simd"))]
157    #[test]
158    fn auto_selection_stays_portable_without_the_simd_feature() {
159        assert_eq!(
160            CpuExecution::detect().instruction_set(),
161            CpuInstructionSet::Portable
162        );
163    }
164
165    #[cfg(feature = "simd")]
166    #[test]
167    fn auto_selection_uses_the_best_runtime_supported_instruction_set() {
168        let backend = CpuExecution::detect();
169        assert_eq!(backend.instruction_set(), backend.features().best());
170        assert!(backend.features().supports(backend.instruction_set()));
171    }
172
173    #[cfg(feature = "parallel")]
174    #[test]
175    fn configured_worker_bound_controls_the_local_pool() {
176        let backend = CpuExecutionBuilder::new()
177            .threads(NonZeroUsize::new(2).unwrap())
178            .build()
179            .expect("two-worker context");
180        assert_eq!(backend.thread_count().get(), 2);
181        assert_eq!(backend.install(rayon::current_num_threads), 2);
182    }
183
184    /// One worker is a pool of one, not the caller's pool (#259): run from
185    /// inside a wider pool, falling through to the caller would show.
186    #[cfg(feature = "parallel")]
187    #[test]
188    fn a_one_thread_context_runs_on_a_pool_of_one() {
189        let caller = rayon::ThreadPoolBuilder::new()
190            .num_threads(3)
191            .build()
192            .expect("caller pool");
193        let built = CpuExecutionBuilder::new()
194            .threads(NonZeroUsize::MIN)
195            .build()
196            .expect("one-worker context");
197        for context in [built, CpuExecution::portable(), CpuExecution::detect()] {
198            assert_eq!(context.thread_count().get(), 1);
199            let width = caller.install(|| context.install(rayon::current_num_threads));
200            assert_eq!(width, 1, "{context:?} ran on the caller's pool");
201            // Clones share the pool rather than each starting their own.
202            let worker = |c: &CpuExecution| c.install(|| std::thread::current().id());
203            assert_eq!(worker(&context), worker(&context.clone()));
204        }
205    }
206
207    #[cfg(not(feature = "parallel"))]
208    #[test]
209    fn multiple_workers_require_the_parallel_feature() {
210        let error = CpuExecutionBuilder::new()
211            .threads(NonZeroUsize::new(2).unwrap())
212            .build()
213            .unwrap_err();
214        assert_eq!(error, CpuConfigError::ParallelFeatureDisabled);
215    }
216}