1#[cfg(not(feature = "std"))]
9use alloc::vec;
10#[cfg(not(feature = "std"))]
11use alloc::vec::Vec;
12
13use core::sync::atomic::{AtomicBool, Ordering};
14
15#[cfg(feature = "parallel")]
16use rayon::prelude::*;
17
18static PARALLELISM_DISABLED: AtomicBool = AtomicBool::new(false);
20
21pub fn disable_global_parallelism() {
26 PARALLELISM_DISABLED.store(true, Ordering::SeqCst);
27}
28
29pub fn enable_global_parallelism() {
31 PARALLELISM_DISABLED.store(false, Ordering::SeqCst);
32}
33
34pub fn is_parallelism_enabled() -> bool {
36 !PARALLELISM_DISABLED.load(Ordering::SeqCst)
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum Par {
42 Seq,
44 #[cfg(feature = "parallel")]
46 Rayon,
47 #[cfg(feature = "parallel")]
49 RayonWith(usize),
50}
51
52#[allow(clippy::derivable_impls)]
55impl Default for Par {
56 fn default() -> Self {
57 #[cfg(feature = "parallel")]
58 {
59 Par::Rayon
60 }
61 #[cfg(not(feature = "parallel"))]
62 {
63 Par::Seq
64 }
65 }
66}
67
68impl Par {
69 #[inline]
71 pub fn is_sequential(&self) -> bool {
72 match self {
73 Par::Seq => true,
74 #[cfg(feature = "parallel")]
75 _ => !is_parallelism_enabled(),
76 }
77 }
78
79 #[cfg(feature = "parallel")]
81 pub fn num_threads(&self) -> usize {
82 if !is_parallelism_enabled() {
83 return 1;
84 }
85
86 match self {
87 Par::Seq => 1,
88 Par::Rayon => rayon::current_num_threads(),
89 Par::RayonWith(n) => *n,
90 }
91 }
92
93 #[cfg(not(feature = "parallel"))]
95 pub fn num_threads(&self) -> usize {
96 1
97 }
98}
99
100#[derive(Debug, Clone, Copy)]
102pub struct ParThreshold {
103 pub min_elements: usize,
105 pub min_work_per_thread: usize,
107}
108
109impl Default for ParThreshold {
110 fn default() -> Self {
111 ParThreshold {
112 min_elements: 4096,
113 min_work_per_thread: 256,
114 }
115 }
116}
117
118impl ParThreshold {
119 pub const fn new(min_elements: usize, min_work_per_thread: usize) -> Self {
121 ParThreshold {
122 min_elements,
123 min_work_per_thread,
124 }
125 }
126
127 #[inline]
129 pub fn should_parallelize(&self, total_work: usize, par: Par) -> bool {
130 if par.is_sequential() {
131 return false;
132 }
133
134 if total_work < self.min_elements {
135 return false;
136 }
137
138 let threads = par.num_threads();
139 if threads <= 1 {
140 return false;
141 }
142
143 total_work / threads >= self.min_work_per_thread
144 }
145}
146
147#[derive(Debug, Clone, Copy)]
149pub struct WorkRange {
150 pub start: usize,
152 pub end: usize,
154}
155
156impl WorkRange {
157 #[inline]
159 pub const fn new(start: usize, end: usize) -> Self {
160 WorkRange { start, end }
161 }
162
163 #[inline]
165 pub const fn len(&self) -> usize {
166 self.end - self.start
167 }
168
169 #[inline]
171 pub const fn is_empty(&self) -> bool {
172 self.start >= self.end
173 }
174}
175
176pub fn partition_work(total: usize, num_threads: usize) -> Vec<WorkRange> {
178 if num_threads == 0 || total == 0 {
179 return vec![];
180 }
181
182 if num_threads == 1 {
183 return vec![WorkRange::new(0, total)];
184 }
185
186 let chunk_size = total.div_ceil(num_threads);
187 let mut ranges = Vec::with_capacity(num_threads);
188
189 let mut start = 0;
190 while start < total {
191 let end = (start + chunk_size).min(total);
192 ranges.push(WorkRange::new(start, end));
193 start = end;
194 }
195
196 ranges
197}
198
199#[inline]
203pub fn for_each_range<F>(total: usize, par: Par, threshold: &ParThreshold, f: F)
204where
205 F: Fn(WorkRange) + Send + Sync,
206{
207 if !threshold.should_parallelize(total, par) {
208 f(WorkRange::new(0, total));
209 return;
210 }
211
212 #[cfg(feature = "parallel")]
213 {
214 let ranges = partition_work(total, dispatch_thread_count(par));
215 run_in_pool(par, move || {
216 ranges.into_par_iter().for_each(f);
217 });
218 }
219
220 #[cfg(not(feature = "parallel"))]
221 {
222 f(WorkRange::new(0, total));
223 }
224}
225
226#[allow(unused_variables)]
230pub fn map_reduce<T, Map, Reduce>(
231 total: usize,
232 par: Par,
233 threshold: &ParThreshold,
234 identity: T,
235 map: Map,
236 reduce: Reduce,
237) -> T
238where
239 T: Clone + Send + Sync,
240 Map: Fn(WorkRange) -> T + Send + Sync,
241 Reduce: Fn(T, T) -> T + Send + Sync,
242{
243 if !threshold.should_parallelize(total, par) {
244 return map(WorkRange::new(0, total));
245 }
246
247 #[cfg(feature = "parallel")]
248 {
249 let ranges = partition_work(total, dispatch_thread_count(par));
250 run_in_pool(par, move || {
251 ranges
252 .into_par_iter()
253 .map(map)
254 .reduce(|| identity.clone(), reduce)
255 })
256 }
257
258 #[cfg(not(feature = "parallel"))]
259 {
260 map(WorkRange::new(0, total))
261 }
262}
263
264pub fn for_each_indexed<F>(total: usize, par: Par, threshold: &ParThreshold, f: F)
266where
267 F: Fn(usize) + Send + Sync,
268{
269 if !threshold.should_parallelize(total, par) {
270 for i in 0..total {
271 f(i);
272 }
273 return;
274 }
275
276 #[cfg(feature = "parallel")]
277 {
278 run_in_pool(par, move || {
279 (0..total).into_par_iter().for_each(f);
280 });
281 }
282
283 #[cfg(not(feature = "parallel"))]
284 {
285 for i in 0..total {
286 f(i);
287 }
288 }
289}
290
291pub trait ThreadPool: Send + Sync {
299 fn num_threads(&self) -> usize;
301
302 fn execute<F>(&self, f: F)
304 where
305 F: FnOnce() + Send + 'static;
306
307 fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
309 where
310 A: FnOnce() -> RA + Send,
311 B: FnOnce() -> RB + Send,
312 RA: Send,
313 RB: Send;
314
315 fn for_each<F>(&self, range: core::ops::Range<usize>, f: F)
317 where
318 F: Fn(usize) + Send + Sync;
319
320 fn map_reduce<T, Map, Reduce>(
322 &self,
323 range: core::ops::Range<usize>,
324 identity: T,
325 map: Map,
326 reduce: Reduce,
327 ) -> T
328 where
329 T: Clone + Send + Sync,
330 Map: Fn(usize) -> T + Send + Sync,
331 Reduce: Fn(T, T) -> T + Send + Sync;
332}
333
334#[derive(Debug, Clone, Copy, Default)]
336pub struct SequentialPool;
337
338impl ThreadPool for SequentialPool {
339 #[inline]
340 fn num_threads(&self) -> usize {
341 1
342 }
343
344 #[inline]
345 fn execute<F>(&self, f: F)
346 where
347 F: FnOnce() + Send + 'static,
348 {
349 f();
350 }
351
352 #[inline]
353 fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
354 where
355 A: FnOnce() -> RA + Send,
356 B: FnOnce() -> RB + Send,
357 RA: Send,
358 RB: Send,
359 {
360 (a(), b())
361 }
362
363 fn for_each<F>(&self, range: core::ops::Range<usize>, f: F)
364 where
365 F: Fn(usize) + Send + Sync,
366 {
367 for i in range {
368 f(i);
369 }
370 }
371
372 fn map_reduce<T, Map, Reduce>(
373 &self,
374 range: core::ops::Range<usize>,
375 identity: T,
376 map: Map,
377 reduce: Reduce,
378 ) -> T
379 where
380 T: Clone + Send + Sync,
381 Map: Fn(usize) -> T + Send + Sync,
382 Reduce: Fn(T, T) -> T + Send + Sync,
383 {
384 let mut acc = identity;
385 for i in range {
386 acc = reduce(acc, map(i));
387 }
388 acc
389 }
390}
391
392#[cfg(feature = "parallel")]
394#[derive(Debug, Clone, Copy, Default)]
395pub struct RayonGlobalPool;
396
397#[cfg(feature = "parallel")]
398impl ThreadPool for RayonGlobalPool {
399 #[inline]
400 fn num_threads(&self) -> usize {
401 rayon::current_num_threads()
402 }
403
404 #[inline]
405 fn execute<F>(&self, f: F)
406 where
407 F: FnOnce() + Send + 'static,
408 {
409 rayon::spawn(f);
410 }
411
412 #[inline]
413 fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
414 where
415 A: FnOnce() -> RA + Send,
416 B: FnOnce() -> RB + Send,
417 RA: Send,
418 RB: Send,
419 {
420 rayon::join(a, b)
421 }
422
423 fn for_each<F>(&self, range: core::ops::Range<usize>, f: F)
424 where
425 F: Fn(usize) + Send + Sync,
426 {
427 range.into_par_iter().for_each(f);
428 }
429
430 fn map_reduce<T, Map, Reduce>(
431 &self,
432 range: core::ops::Range<usize>,
433 identity: T,
434 map: Map,
435 reduce: Reduce,
436 ) -> T
437 where
438 T: Clone + Send + Sync,
439 Map: Fn(usize) -> T + Send + Sync,
440 Reduce: Fn(T, T) -> T + Send + Sync,
441 {
442 range
443 .into_par_iter()
444 .map(map)
445 .reduce(|| identity.clone(), reduce)
446 }
447}
448
449#[cfg(feature = "parallel")]
451pub struct CustomRayonPool {
452 pool: rayon::ThreadPool,
453}
454
455#[cfg(feature = "parallel")]
456impl CustomRayonPool {
457 pub fn new(num_threads: usize) -> Result<Self, rayon::ThreadPoolBuildError> {
459 let pool = rayon::ThreadPoolBuilder::new()
460 .num_threads(num_threads)
461 .build()?;
462 Ok(CustomRayonPool { pool })
463 }
464
465 pub fn with_num_threads(n: usize) -> Result<Self, rayon::ThreadPoolBuildError> {
470 Self::new(n)
471 }
472
473 pub fn with_builder<F>(configure: F) -> Result<Self, rayon::ThreadPoolBuildError>
475 where
476 F: FnOnce(rayon::ThreadPoolBuilder) -> rayon::ThreadPoolBuilder,
477 {
478 let builder = rayon::ThreadPoolBuilder::new();
479 let pool = configure(builder).build()?;
480 Ok(CustomRayonPool { pool })
481 }
482
483 pub fn install<R, F>(&self, f: F) -> R
485 where
486 F: FnOnce() -> R + Send,
487 R: Send,
488 {
489 self.pool.install(f)
490 }
491}
492
493#[cfg(feature = "parallel")]
494impl ThreadPool for CustomRayonPool {
495 #[inline]
496 fn num_threads(&self) -> usize {
497 self.pool.current_num_threads()
498 }
499
500 #[inline]
501 fn execute<F>(&self, f: F)
502 where
503 F: FnOnce() + Send + 'static,
504 {
505 self.pool.spawn(f);
506 }
507
508 #[inline]
509 fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
510 where
511 A: FnOnce() -> RA + Send,
512 B: FnOnce() -> RB + Send,
513 RA: Send,
514 RB: Send,
515 {
516 self.pool.join(a, b)
517 }
518
519 fn for_each<F>(&self, range: core::ops::Range<usize>, f: F)
520 where
521 F: Fn(usize) + Send + Sync,
522 {
523 self.pool.install(|| {
524 range.into_par_iter().for_each(f);
525 });
526 }
527
528 fn map_reduce<T, Map, Reduce>(
529 &self,
530 range: core::ops::Range<usize>,
531 identity: T,
532 map: Map,
533 reduce: Reduce,
534 ) -> T
535 where
536 T: Clone + Send + Sync,
537 Map: Fn(usize) -> T + Send + Sync,
538 Reduce: Fn(T, T) -> T + Send + Sync,
539 {
540 self.pool.install(|| {
541 range
542 .into_par_iter()
543 .map(map)
544 .reduce(|| identity.clone(), reduce)
545 })
546 }
547}
548
549pub struct PoolScope<'a, P: ThreadPool> {
553 pool: &'a P,
554 threshold: ParThreshold,
555}
556
557impl<'a, P: ThreadPool> PoolScope<'a, P> {
558 pub fn new(pool: &'a P) -> Self {
560 PoolScope {
561 pool,
562 threshold: ParThreshold::default(),
563 }
564 }
565
566 pub fn with_threshold(pool: &'a P, threshold: ParThreshold) -> Self {
568 PoolScope { pool, threshold }
569 }
570
571 #[inline]
573 pub fn num_threads(&self) -> usize {
574 self.pool.num_threads()
575 }
576
577 #[inline]
579 pub fn join<A, B, RA, RB>(&self, a: A, b: B) -> (RA, RB)
580 where
581 A: FnOnce() -> RA + Send,
582 B: FnOnce() -> RB + Send,
583 RA: Send,
584 RB: Send,
585 {
586 self.pool.join(a, b)
587 }
588
589 pub fn for_each<F>(&self, total: usize, f: F)
591 where
592 F: Fn(usize) + Send + Sync,
593 {
594 if total < self.threshold.min_elements || self.pool.num_threads() <= 1 {
595 for i in 0..total {
596 f(i);
597 }
598 } else {
599 self.pool.for_each(0..total, f);
600 }
601 }
602
603 pub fn for_each_range<F>(&self, total: usize, f: F)
605 where
606 F: Fn(WorkRange) + Send + Sync,
607 {
608 if total < self.threshold.min_elements || self.pool.num_threads() <= 1 {
609 f(WorkRange::new(0, total));
610 } else {
611 let ranges = partition_work(total, self.pool.num_threads());
618 let num_ranges = ranges.len();
619 self.pool.for_each(0..num_ranges, |idx| f(ranges[idx]));
620 }
621 }
622
623 pub fn map_reduce<T, Map, Reduce>(
625 &self,
626 total: usize,
627 identity: T,
628 map: Map,
629 reduce: Reduce,
630 ) -> T
631 where
632 T: Clone + Send + Sync,
633 Map: Fn(usize) -> T + Send + Sync,
634 Reduce: Fn(T, T) -> T + Send + Sync,
635 {
636 if total < self.threshold.min_elements || self.pool.num_threads() <= 1 {
637 let mut acc = identity;
638 for i in 0..total {
639 acc = reduce(acc, map(i));
640 }
641 acc
642 } else {
643 self.pool.map_reduce(0..total, identity, map, reduce)
644 }
645 }
646}
647
648#[cfg(feature = "parallel")]
650pub fn default_pool() -> RayonGlobalPool {
651 RayonGlobalPool
652}
653
654#[cfg(not(feature = "parallel"))]
656pub fn default_pool() -> SequentialPool {
657 SequentialPool
658}
659
660#[cfg(feature = "parallel")]
664pub fn with_default_pool<R, F>(f: F) -> R
665where
666 F: FnOnce(PoolScope<'_, RayonGlobalPool>) -> R,
667{
668 let pool = RayonGlobalPool;
669 f(PoolScope::new(&pool))
670}
671
672#[cfg(not(feature = "parallel"))]
674pub fn with_default_pool<R, F>(f: F) -> R
675where
676 F: FnOnce(PoolScope<'_, SequentialPool>) -> R,
677{
678 let pool = SequentialPool;
679 f(PoolScope::new(&pool))
680}
681
682#[cfg(feature = "std")]
709#[derive(Debug, Clone, Default)]
710pub struct OxiblasThreadConfig {
711 pub num_threads: usize,
713 pub stack_size: usize,
715 pub thread_name: Option<String>,
717}
718
719#[cfg(feature = "std")]
720impl OxiblasThreadConfig {
721 pub fn new() -> Self {
723 Self::default()
724 }
725
726 pub fn num_threads(mut self, n: usize) -> Self {
728 self.num_threads = n;
729 self
730 }
731
732 pub fn stack_size(mut self, bytes: usize) -> Self {
734 self.stack_size = bytes;
735 self
736 }
737
738 pub fn thread_name(mut self, name: impl Into<String>) -> Self {
740 self.thread_name = Some(name.into());
741 self
742 }
743
744 pub fn effective_threads(&self) -> usize {
747 if self.num_threads == 0 {
748 std::thread::available_parallelism()
749 .map(|n| n.get())
750 .unwrap_or(1)
751 } else {
752 self.num_threads
753 }
754 }
755
756 #[cfg(feature = "parallel")]
760 pub fn build_pool(&self) -> Result<CustomRayonPool, rayon::ThreadPoolBuildError> {
761 let mut builder = rayon::ThreadPoolBuilder::new().num_threads(self.effective_threads());
762 if self.stack_size > 0 {
763 builder = builder.stack_size(self.stack_size);
764 }
765 if let Some(name) = &self.thread_name {
766 let name = name.clone();
767 builder = builder.thread_name(move |i| format!("{name}-{i}"));
768 }
769 let pool = builder.build()?;
770 Ok(CustomRayonPool { pool })
771 }
772}
773
774#[cfg(feature = "parallel")]
788static GLOBAL_POOL: std::sync::OnceLock<CustomRayonPool> = std::sync::OnceLock::new();
789
790#[cfg(all(feature = "std", not(feature = "parallel")))]
793static GLOBAL_POOL: std::sync::OnceLock<SequentialPool> = std::sync::OnceLock::new();
794
795#[cfg(feature = "parallel")]
807fn dispatch_thread_count(par: Par) -> usize {
808 if !is_parallelism_enabled() {
809 return 1;
810 }
811 match par {
812 Par::Seq => 1,
813 Par::RayonWith(n) => n.max(1),
814 Par::Rayon => GLOBAL_POOL
815 .get()
816 .map(|p| p.num_threads())
817 .unwrap_or_else(rayon::current_num_threads),
818 }
819}
820
821#[cfg(feature = "parallel")]
829fn cached_pool(n: usize) -> Option<std::sync::Arc<rayon::ThreadPool>> {
830 use std::collections::HashMap;
831 use std::sync::{Arc, Mutex, OnceLock};
832
833 static CACHE: OnceLock<Mutex<HashMap<usize, Arc<rayon::ThreadPool>>>> = OnceLock::new();
834 let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
835 let mut guard = cache
839 .lock()
840 .unwrap_or_else(|poisoned| poisoned.into_inner());
841 if let Some(pool) = guard.get(&n) {
842 return Some(Arc::clone(pool));
843 }
844 match rayon::ThreadPoolBuilder::new().num_threads(n).build() {
845 Ok(pool) => {
846 let pool = Arc::new(pool);
847 guard.insert(n, Arc::clone(&pool));
848 Some(pool)
849 }
850 Err(_) => None,
851 }
852}
853
854#[cfg(feature = "parallel")]
865fn run_in_pool<R, Op>(par: Par, op: Op) -> R
866where
867 Op: FnOnce() -> R + Send,
868 R: Send,
869{
870 match par {
871 Par::RayonWith(n) => match cached_pool(n) {
872 Some(pool) => pool.install(op),
873 None => op(),
874 },
875 _ => match GLOBAL_POOL.get() {
876 Some(pool) => pool.install(op),
877 None => op(),
878 },
879 }
880}
881
882#[cfg(all(feature = "std", feature = "parallel"))]
905pub fn set_global_thread_pool(pool: CustomRayonPool) {
906 let _ = GLOBAL_POOL.set(pool);
907}
908
909#[cfg(all(feature = "std", not(feature = "parallel")))]
912pub fn set_global_thread_pool(pool: SequentialPool) {
913 let _ = GLOBAL_POOL.set(pool);
914}
915
916#[cfg(feature = "parallel")]
919pub fn global_num_threads() -> usize {
920 GLOBAL_POOL.get().map(|p| p.num_threads()).unwrap_or(1)
921}
922
923#[cfg(all(feature = "std", not(feature = "parallel")))]
927pub fn global_num_threads() -> usize {
928 GLOBAL_POOL.get().map_or(1, |p| p.num_threads())
929}
930
931#[cfg(feature = "parallel")]
948pub fn with_thread_count(n: usize, f: impl FnOnce() + Send) {
949 let pool = rayon::ThreadPoolBuilder::new().num_threads(n).build();
950 match pool {
951 Ok(p) => p.install(f),
952 Err(_) => f(), }
954}
955
956#[cfg(not(feature = "parallel"))]
958pub fn with_thread_count(_n: usize, f: impl FnOnce()) {
959 f();
960}
961
962#[cfg(feature = "parallel")]
985pub struct ThreadLocalAccum<T> {
986 values: Vec<std::sync::Mutex<T>>,
987}
988
989#[cfg(feature = "parallel")]
990impl<T: Clone + Send> ThreadLocalAccum<T> {
991 pub fn new(identity: T) -> Self {
993 let num_threads = rayon::current_num_threads().max(1);
998 let values = (0..num_threads)
999 .map(|_| std::sync::Mutex::new(identity.clone()))
1000 .collect();
1001 ThreadLocalAccum { values }
1002 }
1003
1004 pub fn get(&self) -> std::sync::MutexGuard<'_, T> {
1006 let thread_idx = rayon::current_thread_index().unwrap_or(0) % self.values.len();
1007 self.values[thread_idx]
1008 .lock()
1009 .unwrap_or_else(|poisoned| poisoned.into_inner())
1010 }
1011
1012 pub fn reduce<F>(self, f: F) -> T
1018 where
1019 F: Fn(T, T) -> T,
1020 {
1021 let mut acc: Option<T> = None;
1022 for shard in self.values {
1023 let value = shard
1024 .into_inner()
1025 .unwrap_or_else(|poisoned| poisoned.into_inner());
1026 acc = Some(match acc {
1027 Some(previous) => f(previous, value),
1028 None => value,
1029 });
1030 }
1031 acc.expect("ThreadLocalAccum always holds at least one shard")
1036 }
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041 use super::*;
1042
1043 #[test]
1044 fn test_partition_work() {
1045 let ranges = partition_work(100, 4);
1046 assert_eq!(ranges.len(), 4);
1047
1048 let mut covered = [false; 100];
1050 for range in &ranges {
1051 for (offset, covered_elem) in covered[range.start..range.end].iter_mut().enumerate() {
1052 let i = range.start + offset;
1053 assert!(!*covered_elem, "Overlap at {}", i);
1054 *covered_elem = true;
1055 }
1056 }
1057 assert!(covered.iter().all(|&x| x), "Not all elements covered");
1058 }
1059
1060 #[test]
1061 fn test_partition_work_uneven() {
1062 let ranges = partition_work(10, 4);
1063
1064 let total: usize = ranges.iter().map(|r| r.len()).sum();
1066 assert_eq!(total, 10);
1067 }
1068
1069 #[test]
1070 fn test_partition_work_single() {
1071 let ranges = partition_work(100, 1);
1072 assert_eq!(ranges.len(), 1);
1073 assert_eq!(ranges[0].start, 0);
1074 assert_eq!(ranges[0].end, 100);
1075 }
1076
1077 #[test]
1078 fn test_threshold() {
1079 let threshold = ParThreshold::new(100, 10);
1080
1081 assert!(!threshold.should_parallelize(50, Par::Seq));
1082 assert!(!threshold.should_parallelize(50, Par::default()));
1083
1084 #[cfg(feature = "parallel")]
1085 {
1086 assert!(threshold.should_parallelize(1000, Par::Rayon));
1088 }
1089 }
1090
1091 #[test]
1092 fn test_global_parallelism() {
1093 let was_enabled = is_parallelism_enabled();
1095
1096 disable_global_parallelism();
1097 assert!(!is_parallelism_enabled());
1098
1099 enable_global_parallelism();
1100 assert!(is_parallelism_enabled());
1101
1102 if !was_enabled {
1104 disable_global_parallelism();
1105 }
1106 }
1107
1108 #[test]
1109 fn test_sequential_map_reduce() {
1110 let result = map_reduce(
1111 100,
1112 Par::Seq,
1113 &ParThreshold::default(),
1114 0usize,
1115 |range| range.len(),
1116 |a, b| a + b,
1117 );
1118 assert_eq!(result, 100);
1119 }
1120
1121 #[test]
1123 fn test_sequential_pool() {
1124 let pool = SequentialPool;
1125
1126 assert_eq!(pool.num_threads(), 1);
1127
1128 let (a, b) = pool.join(|| 1 + 1, || 2 + 2);
1130 assert_eq!(a, 2);
1131 assert_eq!(b, 4);
1132
1133 let sum = core::sync::atomic::AtomicUsize::new(0);
1135 pool.for_each(0..10, |i| {
1136 sum.fetch_add(i, core::sync::atomic::Ordering::SeqCst);
1137 });
1138 assert_eq!(sum.load(core::sync::atomic::Ordering::SeqCst), 45);
1139
1140 let result = pool.map_reduce(0..10, 0, |i| i, |a, b| a + b);
1142 assert_eq!(result, 45);
1143 }
1144
1145 #[test]
1146 fn test_pool_scope() {
1147 let pool = SequentialPool;
1148 let scope = PoolScope::new(&pool);
1149
1150 assert_eq!(scope.num_threads(), 1);
1151
1152 let result = scope.map_reduce(100, 0usize, |i| i, |a, b| a + b);
1154 assert_eq!(result, (0..100).sum::<usize>());
1155
1156 let sum = core::sync::atomic::AtomicUsize::new(0);
1158 scope.for_each(10, |i| {
1159 sum.fetch_add(i, core::sync::atomic::Ordering::SeqCst);
1160 });
1161 assert_eq!(sum.load(core::sync::atomic::Ordering::SeqCst), 45);
1162 }
1163
1164 #[test]
1165 fn test_pool_scope_with_threshold() {
1166 let pool = SequentialPool;
1167 let threshold = ParThreshold::new(50, 10);
1168 let scope = PoolScope::with_threshold(&pool, threshold);
1169
1170 let result = scope.map_reduce(100, 0usize, |i| i, |a, b| a + b);
1172 assert_eq!(result, (0..100).sum::<usize>());
1173 }
1174
1175 #[test]
1176 fn test_default_pool() {
1177 let pool = default_pool();
1178 assert!(pool.num_threads() >= 1);
1180 }
1181
1182 #[test]
1183 fn test_with_default_pool() {
1184 let result = with_default_pool(|scope| scope.num_threads());
1185 assert!(result >= 1);
1186 }
1187
1188 #[cfg(feature = "parallel")]
1189 #[test]
1190 fn test_rayon_global_pool() {
1191 let pool = RayonGlobalPool;
1192
1193 assert!(pool.num_threads() >= 1);
1195
1196 let (a, b) = pool.join(|| 1 + 1, || 2 + 2);
1198 assert_eq!(a, 2);
1199 assert_eq!(b, 4);
1200
1201 let result = pool.map_reduce(0..100, 0, |i| i, |a, b| a + b);
1203 assert_eq!(result, (0..100).sum::<usize>());
1204 }
1205
1206 #[cfg(feature = "parallel")]
1207 #[test]
1208 fn test_custom_rayon_pool() {
1209 let pool = CustomRayonPool::new(2).expect("Failed to create pool");
1210
1211 assert_eq!(pool.num_threads(), 2);
1212
1213 let result = pool.map_reduce(0..100, 0, |i| i, |a, b| a + b);
1215 assert_eq!(result, (0..100).sum::<usize>());
1216
1217 let result = pool.install(|| (0..100).into_par_iter().sum::<usize>());
1219 assert_eq!(result, (0..100).sum());
1220 }
1221
1222 #[cfg(feature = "std")]
1225 #[test]
1226 fn test_thread_config_default() {
1227 let cfg = OxiblasThreadConfig::default();
1228 assert_eq!(cfg.num_threads, 0);
1229 assert_eq!(cfg.stack_size, 0);
1230 assert!(cfg.thread_name.is_none());
1231 }
1232
1233 #[cfg(feature = "std")]
1234 #[test]
1235 fn test_thread_config_builder() {
1236 let cfg = OxiblasThreadConfig::new()
1237 .num_threads(4)
1238 .stack_size(1024 * 1024)
1239 .thread_name("oxiblas-worker");
1240 assert_eq!(cfg.num_threads, 4);
1241 assert_eq!(cfg.stack_size, 1024 * 1024);
1242 assert_eq!(cfg.thread_name.as_deref(), Some("oxiblas-worker"));
1243 }
1244
1245 #[cfg(feature = "std")]
1246 #[test]
1247 fn test_thread_config_effective_threads_zero() {
1248 let cfg = OxiblasThreadConfig::new().num_threads(0);
1249 assert!(cfg.effective_threads() >= 1);
1251 }
1252
1253 #[cfg(feature = "std")]
1254 #[test]
1255 fn test_thread_config_effective_threads_explicit() {
1256 let cfg = OxiblasThreadConfig::new().num_threads(3);
1257 assert_eq!(cfg.effective_threads(), 3);
1258 }
1259
1260 #[cfg(feature = "parallel")]
1261 #[test]
1262 fn test_custom_rayon_pool_with_num_threads() {
1263 let pool = CustomRayonPool::with_num_threads(2).expect("build pool");
1264 assert_eq!(pool.num_threads(), 2);
1265 let sum: usize = pool.map_reduce(0..50, 0, |i| i, |a, b| a + b);
1266 assert_eq!(sum, (0..50).sum::<usize>());
1267 }
1268
1269 #[cfg(feature = "parallel")]
1270 #[test]
1271 fn test_oxiblas_thread_config_build_pool() {
1272 let cfg = OxiblasThreadConfig::new().num_threads(2);
1273 let pool = cfg.build_pool().expect("build pool");
1274 assert_eq!(pool.num_threads(), 2);
1275 }
1276
1277 #[cfg(feature = "parallel")]
1278 #[test]
1279 fn test_with_thread_count() {
1280 with_thread_count(2, || {
1282 assert_eq!(rayon::current_num_threads(), 2);
1283 });
1284 }
1285
1286 #[cfg(not(feature = "parallel"))]
1287 #[test]
1288 fn test_with_thread_count_sequential() {
1289 let mut called = false;
1291 with_thread_count(4, || {
1292 called = true;
1293 });
1294 assert!(called);
1295 }
1296
1297 #[cfg(feature = "std")]
1298 #[test]
1299 fn test_global_num_threads_default() {
1300 assert!(global_num_threads() >= 1);
1303 }
1304
1305 #[cfg(feature = "parallel")]
1316 #[test]
1317 fn test_rayon_with_installs_n_thread_pool() {
1318 let observed3 = run_in_pool(Par::RayonWith(3), rayon::current_num_threads);
1319 assert_eq!(observed3, 3, "RayonWith(3) did not install a 3-thread pool");
1320 let observed5 = run_in_pool(Par::RayonWith(5), rayon::current_num_threads);
1321 assert_eq!(observed5, 5, "RayonWith(5) did not install a 5-thread pool");
1322 let again = run_in_pool(Par::RayonWith(3), rayon::current_num_threads);
1324 assert_eq!(again, 3, "cached RayonWith(3) pool changed size");
1325 }
1326
1327 #[cfg(feature = "parallel")]
1330 #[test]
1331 fn test_for_each_range_rayon_with_covers_domain() {
1332 enable_global_parallelism();
1333 let covered = core::sync::atomic::AtomicUsize::new(0);
1334 let low = ParThreshold::new(1, 1);
1335 for_each_range(4_096, Par::RayonWith(4), &low, |range| {
1336 covered.fetch_add(range.len(), core::sync::atomic::Ordering::SeqCst);
1337 });
1338 assert_eq!(
1339 covered.load(core::sync::atomic::Ordering::SeqCst),
1340 4_096,
1341 "RayonWith ranges did not tile the domain"
1342 );
1343 }
1344
1345 #[cfg(feature = "parallel")]
1352 #[test]
1353 fn test_global_pool_executes_on_registered_pool() {
1354 let pool = OxiblasThreadConfig::new()
1355 .num_threads(3)
1356 .thread_name("oxiblas-global-test")
1357 .build_pool()
1358 .expect("build named global pool");
1359 assert_eq!(pool.num_threads(), 3);
1360 set_global_thread_pool(pool);
1361 assert_eq!(
1362 global_num_threads(),
1363 3,
1364 "registered pool size not reflected"
1365 );
1366
1367 let names = std::sync::Mutex::new(std::collections::HashSet::new());
1368 run_in_pool(Par::Rayon, || {
1369 (0..4_096usize).into_par_iter().for_each(|_| {
1370 if let Some(name) = std::thread::current().name() {
1371 names
1372 .lock()
1373 .unwrap_or_else(|poisoned| poisoned.into_inner())
1374 .insert(name.to_string());
1375 }
1376 });
1377 });
1378 let names = names
1379 .into_inner()
1380 .unwrap_or_else(|poisoned| poisoned.into_inner());
1381 assert!(
1382 !names.is_empty(),
1383 "no named worker observed; work did not run on the registered pool"
1384 );
1385 assert!(
1386 names.iter().all(|n| n.starts_with("oxiblas-global-test")),
1387 "Par::Rayon ran on unexpected threads: {names:?}"
1388 );
1389 }
1390
1391 #[cfg(feature = "parallel")]
1396 #[test]
1397 fn test_pool_scope_for_each_range_runs_on_pool() {
1398 let pool = OxiblasThreadConfig::new()
1399 .num_threads(3)
1400 .thread_name("oxiblas-scope-test")
1401 .build_pool()
1402 .expect("build named scope pool");
1403 let scope = PoolScope::with_threshold(&pool, ParThreshold::new(1, 1));
1404
1405 let names = std::sync::Mutex::new(std::collections::HashSet::new());
1406 let covered = core::sync::atomic::AtomicUsize::new(0);
1407 let total = 96usize;
1408 scope.for_each_range(total, |range| {
1409 covered.fetch_add(range.len(), core::sync::atomic::Ordering::SeqCst);
1410 let name = std::thread::current().name().map(str::to_string);
1411 names
1412 .lock()
1413 .unwrap_or_else(|poisoned| poisoned.into_inner())
1414 .insert(name);
1415 });
1416 let names = names
1417 .into_inner()
1418 .unwrap_or_else(|poisoned| poisoned.into_inner());
1419 assert!(
1420 names.iter().all(|n| n
1421 .as_deref()
1422 .is_some_and(|n| n.starts_with("oxiblas-scope-test"))),
1423 "ranges executed off-pool (likely on the caller thread): {names:?}"
1424 );
1425 assert_eq!(
1426 covered.load(core::sync::atomic::Ordering::SeqCst),
1427 total,
1428 "PoolScope ranges did not tile the domain"
1429 );
1430 }
1431
1432 #[cfg(feature = "parallel")]
1436 #[test]
1437 fn test_thread_local_accum_reduce_infallible() {
1438 let empty = ThreadLocalAccum::new(0i64);
1441 assert_eq!(empty.reduce(|a, b| a + b), 0);
1442
1443 let accum = ThreadLocalAccum::new(0i64);
1445 (0..1_000i64).into_par_iter().for_each(|x| {
1446 let mut shard = accum.get();
1447 *shard += x;
1448 });
1449 assert_eq!(accum.reduce(|a, b| a + b), (0..1_000i64).sum::<i64>());
1450 }
1451}