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}