Skip to main content

fso_graph/
dss.rs

1use std::{mem::ManuallyDrop, sync::atomic::Ordering};
2
3use rayon::prelude::*;
4
5use crate::prelude::*;
6
7/// A thread-safe Disjoint Set Struct implementation, that
8/// can be safely shared and accessed across threads.
9///
10/// The implementation is based on the Java implementation [1]
11/// which in turn is based on a C++ implementation and some
12/// input from a Rust implementation [3].
13///
14/// This implementation is tailored for the graph crate as
15/// it needs to support the `Idx` trait.
16///
17/// [1] [Java](https://github.com/neo4j/graph-data-science/blob/edeab6ab68f241135737aafe68a113248f11042c/core/src/main/java/org/neo4j/gds/core/utils/paged/dss/HugeAtomicDisjointSetStruct.java)
18/// [2] [C++](https://github.com/wjakob/dset/blob/7967ef0e6041cd9d73b9c7f614ab8ae92e9e587a/dset.h)
19/// [3] [Rust](https://github.com/tov/disjoint-sets-rs/blob/88ab08df21f04fcf7c157b6e042efd561ee873ba/src/concurrent.rs)
20pub struct DisjointSetStruct<NI: Idx>(Box<[Atomic<NI>]>);
21
22unsafe impl<NI: Idx> Sync for DisjointSetStruct<NI> {}
23unsafe impl<NI: Idx> Send for DisjointSetStruct<NI> {}
24
25impl<NI: Idx> UnionFind<NI> for DisjointSetStruct<NI> {
26    /// Joins the set of `id1` with the set of `id2`.
27    ///
28    /// # Examples
29    ///
30    /// ```
31    /// use fso_graph::prelude::*;
32    ///
33    /// let dss = DisjointSetStruct::new(10);
34    /// dss.union(2, 4);
35    /// assert_eq!(dss.find(2), 2);
36    /// assert_eq!(dss.find(4), 2);
37    /// ```
38    fn union(&self, mut id1: NI, mut id2: NI) {
39        loop {
40            id1 = self.find(id1);
41            id2 = self.find(id2);
42
43            if id1 == id2 {
44                return;
45            }
46
47            // We do Union-by-Min, so the smaller set id wins.
48            // We also only update the entry for id1 and if that
49            // is the smaller value, we need to swap ids so we update
50            // only the value for id2, not id1.
51            if id1 < id2 {
52                std::mem::swap(&mut id1, &mut id2);
53            }
54
55            let old_entry = id1;
56            let new_entry = id2;
57
58            if self.update_parent(id1, old_entry, new_entry).is_ok() {
59                break;
60            }
61        }
62    }
63
64    /// Find the set of `id`.
65    ///
66    /// # Examples
67    ///
68    /// ```
69    /// use fso_graph::prelude::*;
70    ///
71    /// let dss = DisjointSetStruct::new(10);
72    /// assert_eq!(dss.find(4), 4);
73    /// dss.union(4, 2);
74    /// assert_eq!(dss.find(4), 2);
75    /// ```
76    fn find(&self, mut id: NI) -> NI {
77        let mut parent = self.parent(id);
78
79        while id != parent {
80            let grand_parent = self.parent(parent);
81            // Try to apply path-halving by setting the value
82            // for some id to its grand parent. This might fail
83            // if another thread is also changing the same value
84            // but that's ok. The CAS operations guarantees
85            // that at least one of the contenting threads will
86            // succeed. That's enough for the path-halving to work
87            // and there is no need to retry in case of a CAS failure.
88            let _ = self.update_parent(id, parent, grand_parent);
89            id = parent;
90            parent = grand_parent;
91        }
92
93        id
94    }
95
96    /// Returns the number of elements in the dss, also referred to
97    /// as its 'length'.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use fso_graph::prelude::*;
103    ///
104    /// let dss = DisjointSetStruct::<usize>::new(3);
105    /// assert_eq!(dss.len(), 3);
106    /// ```
107    fn len(&self) -> usize {
108        self.0.len()
109    }
110
111    /// Compresses the DSS so that each id stores its root set id.
112    fn compress(&self) {
113        (0..self.len()).into_par_iter().map(NI::new).for_each(|id| {
114            self.find(id);
115        });
116    }
117}
118
119impl<NI: Idx> DisjointSetStruct<NI> {
120    /// Creates a new disjoint-set struct of `size` elements.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use fso_graph::prelude::*;
126    ///
127    /// let dss = DisjointSetStruct::new(3);
128    /// dss.union(0, 1);
129    /// let set0 = dss.find(0);
130    /// let set1 = dss.find(1);
131    /// assert_eq!(set0, set1);
132    /// ```
133    pub fn new(size: usize) -> Self {
134        let mut v = Vec::with_capacity(size);
135
136        (0..size)
137            .into_par_iter()
138            .map(|i| Atomic::new(NI::new(i)))
139            .collect_into_vec(&mut v);
140
141        Self(v.into_boxed_slice())
142    }
143
144    fn parent(&self, i: NI) -> NI {
145        self.0[i.index()].load(Ordering::SeqCst)
146    }
147
148    fn update_parent(&self, id: NI, current: NI, new: NI) -> Result<NI, NI> {
149        self.0[id.index()].compare_exchange_weak(current, new, Ordering::SeqCst, Ordering::Relaxed)
150    }
151}
152
153impl<NI: Idx> Components<NI> for DisjointSetStruct<NI> {
154    fn component(&self, node: NI) -> NI {
155        self.find(node)
156    }
157
158    fn to_vec(self) -> Vec<NI> {
159        let mut components = ManuallyDrop::new(self.0.into_vec());
160        let (ptr, len, cap) = (
161            components.as_mut_ptr(),
162            components.len(),
163            components.capacity(),
164        );
165
166        // SAFETY: NI and NI::Atomic have the same memory layout
167        unsafe {
168            let ptr = ptr as *mut Vec<NI>;
169            let ptr = ptr as *mut _;
170            Vec::from_raw_parts(ptr, len, cap)
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use std::sync::Arc;
178    use std::sync::Barrier;
179
180    use super::*;
181
182    #[test]
183    fn test_union() {
184        let dss = DisjointSetStruct::new(10);
185
186        assert_eq!(dss.find(9), 9);
187        dss.union(9, 7);
188        assert_eq!(dss.find(9), 7);
189        dss.union(7, 4);
190        assert_eq!(dss.find(9), 4);
191        dss.union(4, 2);
192        assert_eq!(dss.find(9), 2);
193        dss.union(2, 0);
194        assert_eq!(dss.find(9), 0);
195    }
196
197    #[test]
198    fn test_union_with_path_halving() {
199        let dss = DisjointSetStruct::new(10);
200
201        dss.union(4, 3);
202        dss.union(3, 2);
203        dss.union(2, 1);
204        dss.union(1, 0);
205
206        dss.union(9, 8);
207        dss.union(8, 7);
208        dss.union(7, 6);
209        dss.union(6, 5);
210
211        assert_eq!(dss.find(4), 0);
212        assert_eq!(dss.find(9), 5);
213
214        dss.union(5, 4);
215
216        for i in 0..dss.len() {
217            assert_eq!(dss.find(i), 0);
218        }
219    }
220
221    #[test]
222    fn test_union_parallel() {
223        let barrier = Arc::new(Barrier::new(2));
224        let dss = Arc::new(DisjointSetStruct::new(1000));
225
226        fn workload(barrier: &Barrier, dss: &DisjointSetStruct<u64>) {
227            barrier.wait();
228            for i in 0..500 {
229                dss.union(i, i + 1);
230            }
231            // We wait again after the first cluster to increase the chance of concurrent updates
232            barrier.wait();
233            for i in 501..999 {
234                dss.union(i, i + 1);
235            }
236        }
237
238        let t1 = std::thread::spawn({
239            let barrier = Arc::clone(&barrier);
240            let dss = Arc::clone(&dss);
241            move || workload(&barrier, &dss)
242        });
243
244        let t2 = std::thread::spawn({
245            let barrier = Arc::clone(&barrier);
246            let dss = Arc::clone(&dss);
247            move || workload(&barrier, &dss)
248        });
249
250        t1.join().unwrap();
251        t2.join().unwrap();
252
253        for i in 0..500 {
254            assert_eq!(dss.find(i), dss.find(i + 1));
255        }
256
257        assert_ne!(dss.find(500), dss.find(501));
258
259        for i in 501..999 {
260            assert_eq!(dss.find(i), dss.find(i + 1));
261        }
262    }
263}