openmp_reducer/lib.rs
1/*
2 * SPDX-FileCopyrightText: 2025 Tommaso Fontana
3 * SPDX-FileCopyrightText: 2025 Sebastiano Vigna
4 *
5 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6 */
7
8#![doc = include_str!("../README.md")]
9
10use std::fmt::{self, Debug};
11use std::marker::PhantomData;
12use std::sync::Mutex;
13
14/// An OpenMP-style reducer that wraps a global value into a [`Mutex`],
15/// providing [shareable, cloneable copies with a local
16/// value](Reducer::share); the copies will be reduced into the global value
17/// when dropped.
18///
19/// The global value can be observed with [`peek`](Reducer::peek) if the base
20/// type is [`Clone`], whereas [`get`](Reducer::get) consumes self and returns
21/// the global value.
22///
23/// For convenience, the global value and the local value have distinct type
24/// parameters `G` and `L`, respectively; the second type defaults to the first
25/// one. The reduction function has type parameter `F`, defaulting to
26/// `fn(&mut G, &L)`.
27///
28/// Each shared copy has a reference to the [`Reducer`] it was created from, so
29/// you cannot call [`get`](Reducer::get) if there are still shared copies
30/// around. For example, this code will not compile:
31/// ```compile_fail
32/// use openmp_reducer::Reducer;
33///
34/// let reducer = Reducer::<usize>::new(3, |global, local| *global += *local);
35/// let mut shared = reducer.share();
36/// // drop(shared); // uncommenting this line would make the code compile
37/// assert_eq!(reducer.get(), 3);
38///```
39///
40/// # Thread Safety
41///
42/// [`Reducer`] is [`Send`] and [`Sync`] when `G: Send` and `F: Send + Sync`.
43/// [`SharedReducer`] is [`Send`] when additionally `L: Send`.
44///
45/// # Examples
46///
47/// In this example, we manually spawn threads:
48///
49/// ```rust
50/// use openmp_reducer::Reducer;
51/// use std::thread;
52///
53/// let reducer = Reducer::<usize>::new(5, |global, local| *global += *local);
54/// std::thread::scope(|s| {
55/// for i in 0..3 {
56/// let mut shared = reducer.share();
57/// s.spawn(move || {
58/// *shared.as_mut() += 10;
59/// });
60/// }
61/// });
62///
63/// // Initial value plus additional values from shared copies
64/// assert_eq!(reducer.get(), 35);
65/// ```
66///
67/// You can obtain the same behavior with [Rayon](https://docs.rs/rayon) using
68/// methods such as
69/// [`for_each_with`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.for_each_with)
70/// and
71/// [`map_with`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.map_with):
72///
73/// ```rust
74/// use openmp_reducer::Reducer;
75/// use rayon::prelude::*;
76///
77/// let reducer = Reducer::<usize>::new(5, |global, local| *global += *local);
78/// (0..1000000).into_par_iter().
79/// with_min_len(1000). // optional, might reduce the amount of cloning
80/// for_each_with(reducer.share(), |shared, i| {
81/// *shared.as_mut() += 1;
82/// }
83/// );
84///
85/// // Initial value plus additional values from clones
86/// assert_eq!(reducer.get(), 1_000_005);
87/// ```
88///
89/// Note that you have to pass `reducer.share()`, which can be cloned. Also,
90/// since
91/// [`for_each_with`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.for_each_with)
92/// might perform excessive cloning if jobs are too short, you can use
93/// [`with_min_len`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.with_min_len)
94/// to reduce the amount of cloning.
95pub struct Reducer<G, L = G, F = fn(&mut G, &L)> {
96 global: Mutex<G>,
97 reduce: F,
98 _marker: PhantomData<fn(L)>,
99}
100
101impl<G: Debug, L, F> Debug for Reducer<G, L, F> {
102 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 f.debug_struct("Reducer")
104 .field("global", &self.global)
105 .finish_non_exhaustive()
106 }
107}
108
109impl<G, L, F: Fn(&mut G, &L)> Reducer<G, L, F> {
110 /// Creates a new reducer with a given reduction function.
111 ///
112 /// The function must reduce the local value (second argument) into the
113 /// global value (first argument). For the result to be deterministic, the
114 /// global value must be the same regardless of the order in which the local
115 /// values are reduced.
116 pub const fn new(init: G, reduce: F) -> Self {
117 Reducer {
118 global: Mutex::new(init),
119 reduce,
120 _marker: PhantomData,
121 }
122 }
123}
124
125impl<G, L, F> Reducer<G, L, F> {
126 /// Consumes self and returns the global value.
127 ///
128 /// Note that you cannot call this method if there are still [shared
129 /// copies](Reducer::share) that have not been dropped.
130 ///
131 /// If you just need to access the global value without consuming self, and
132 /// the base type is [`Clone`], use [`peek`](Reducer::peek).
133 ///
134 /// If the mutex is poisoned (e.g., due to a panic in the reduction
135 /// function), the global value is recovered and returned anyway, mirroring
136 /// the behavior of the [`Drop`] implementation of [`SharedReducer`].
137 pub fn get(self) -> G {
138 self.global.into_inner().unwrap_or_else(|e| e.into_inner())
139 }
140}
141
142impl<G, L: Default, F: Fn(&mut G, &L)> Reducer<G, L, F> {
143 /// Returns a [`SharedReducer`] referencing this [`Reducer`].
144 ///
145 /// The [`SharedReducer`] will be initialized with the default value of the
146 /// local type.
147 pub fn share(&self) -> SharedReducer<'_, G, L, F> {
148 SharedReducer {
149 openmp_reducer: self,
150 local: L::default(),
151 }
152 }
153}
154
155impl<G: Clone, L, F> Reducer<G, L, F> {
156 /// Returns the current global value.
157 ///
158 /// Note that this method does not guarantee that all shared copies have
159 /// been dropped. If you need that guarantee, use [`get`](Reducer::get).
160 ///
161 /// If the mutex is poisoned (e.g., due to a panic in the reduction
162 /// function), the global value is recovered and returned anyway, mirroring
163 /// the behavior of the [`Drop`] implementation of [`SharedReducer`].
164 pub fn peek(&self) -> G {
165 self.global
166 .lock()
167 .unwrap_or_else(|e| e.into_inner())
168 .clone()
169 }
170}
171
172/// A shareable copy of a [`Reducer`] containing a local value and implementing
173/// [`Clone`].
174///
175/// The local value can be accessed with [`AsRef`] and [`AsMut`]
176/// implementations.
177///
178/// When a [`SharedReducer`] is dropped, the local value will be reduced into
179/// the global value. If the mutex is poisoned (e.g., due to a panic in another
180/// thread), the reduction will still be performed on the recovered value.
181pub struct SharedReducer<'a, G, L = G, F: Fn(&mut G, &L) = fn(&mut G, &L)> {
182 openmp_reducer: &'a Reducer<G, L, F>,
183 local: L,
184}
185
186impl<G: Debug, L: Debug, F: Fn(&mut G, &L)> Debug for SharedReducer<'_, G, L, F> {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.debug_struct("SharedReducer")
189 .field("openmp_reducer", &self.openmp_reducer)
190 .field("local", &self.local)
191 .finish()
192 }
193}
194
195impl<G, L: Default, F: Fn(&mut G, &L)> Clone for SharedReducer<'_, G, L, F> {
196 /// Returns a copy sharing the same global value and
197 /// with local value initialized to the default value.
198 fn clone(&self) -> Self {
199 SharedReducer {
200 openmp_reducer: self.openmp_reducer,
201 local: L::default(),
202 }
203 }
204}
205
206impl<G: Clone, L, F: Fn(&mut G, &L)> SharedReducer<'_, G, L, F> {
207 /// Returns the current global value.
208 ///
209 /// This method delegates to [`Reducer::peek`].
210 pub fn peek(&self) -> G {
211 self.openmp_reducer.peek()
212 }
213}
214
215impl<G, L, F: Fn(&mut G, &L)> Drop for SharedReducer<'_, G, L, F> {
216 /// Reduces the local value into the global value.
217 fn drop(&mut self) {
218 let mut lock = self
219 .openmp_reducer
220 .global
221 .lock()
222 .unwrap_or_else(|e| e.into_inner());
223 (self.openmp_reducer.reduce)(&mut *lock, &self.local);
224 }
225}
226
227impl<G, L, F: Fn(&mut G, &L)> AsRef<L> for SharedReducer<'_, G, L, F> {
228 /// Returns a reference to the local value.
229 fn as_ref(&self) -> &L {
230 &self.local
231 }
232}
233
234impl<G, L, F: Fn(&mut G, &L)> AsMut<L> for SharedReducer<'_, G, L, F> {
235 /// Returns a mutable reference to the local value.
236 fn as_mut(&mut self) -> &mut L {
237 &mut self.local
238 }
239}