axiolid_backend_cpu/
execution.rs1use core::fmt;
4use std::num::NonZeroUsize;
5#[cfg(feature = "parallel")]
6use std::sync::{Arc, OnceLock};
7
8use crate::{CpuConfigError, CpuFeatures, CpuInstructionSet};
9
10#[derive(Clone)]
17pub struct CpuExecution {
18 instruction_set: CpuInstructionSet,
19 features: CpuFeatures,
20 threads: NonZeroUsize,
21 #[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 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 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 #[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 pub const fn instruction_set(&self) -> CpuInstructionSet {
97 self.instruction_set
98 }
99
100 pub const fn features(&self) -> CpuFeatures {
102 self.features
103 }
104
105 pub const fn thread_count(&self) -> NonZeroUsize {
107 self.threads
108 }
109
110 #[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#[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 #[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 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}