Skip to main content

p3_matrix/
util.rs

1use core::borrow::BorrowMut;
2
3use p3_maybe_rayon::PARALLEL_ENABLED;
4use p3_maybe_rayon::prelude::*;
5use p3_util::{log2_strict_usize, reverse_bits_len, reverse_slice_index_bits};
6use tracing::instrument;
7
8use crate::Matrix;
9use crate::dense::{DenseMatrix, DenseStorage, RowMajorMatrix};
10
11/// Reverse the order of matrix rows based on the bit-reversal of their indices.
12///
13/// Given a matrix `mat` of height `h = 2^k`, this function rearranges its rows by
14/// reversing the binary representation of each row index. For example, if `h = 8` (i.e., 3 bits):
15///
16/// ```text
17/// Original Index  Binary   Reversed   Target Index
18/// --------------  -------  ---------  -------------
19///      0          000      000        0
20///      1          001      100        4
21///      2          010      010        2
22///      3          011      110        6
23///      4          100      001        1
24///      5          101      101        5
25///      6          110      011        3
26///      7          111      111        7
27/// ```
28///
29/// The transformation is performed in-place.
30///
31/// # Panics
32/// Panics if the height of the matrix is not a power of two.
33///
34/// # Arguments
35/// - `mat`: The matrix whose rows should be reordered.
36#[instrument(level = "debug", skip_all)]
37pub fn reverse_matrix_index_bits<'a, F, S>(mat: &mut DenseMatrix<F, S>)
38where
39    F: Clone + Send + Sync + 'a,
40    S: DenseStorage<F> + BorrowMut<[F]>,
41{
42    let w = mat.width();
43    let h = mat.height();
44    let log_h = log2_strict_usize(h);
45    let values = mat.values.borrow_mut();
46    let total_bytes = core::mem::size_of_val(values);
47
48    // Small elements benefit from the slice permutation's cache-aware decomposition.
49    // On AArch64, retain parallel row swaps above 64 KiB when multiple workers are active.
50    // Check the worker count because dependencies can enable Rayon through feature unification.
51    let use_slice = w == 1 && core::mem::size_of::<F>() <= 8;
52    #[cfg(target_arch = "aarch64")]
53    let use_slice = use_slice && (total_bytes <= 64 * 1024 || current_num_threads() == 1);
54    if use_slice {
55        reverse_slice_index_bits(values);
56        return;
57    }
58
59    let values = values.as_mut_ptr() as usize;
60
61    // SAFETY: Bit reversal is an involution on 0..h. Visiting each index once and
62    // swapping only when i < j gives each row pair a single owner, so concurrent
63    // swaps use disjoint rows within the matrix.
64    let swap = |i, use_outline| {
65        let values = values as *mut F;
66        let j = reverse_bits_len(i, log_h);
67        if i < j {
68            unsafe { swap_rows_raw(values, w, i, j, use_outline) };
69        }
70    };
71    // Total matrix bytes, not rows: avoid scheduling overhead for inputs up to 32 KiB.
72    if total_bytes <= 32 * 1024 {
73        (0..h).for_each(|i| swap(i, true));
74    } else {
75        // Amortize scheduling over roughly 64 KiB of candidate rows; swap partners may
76        // lie outside the chunk.
77        const CHUNK_BYTES: usize = 64 * 1024;
78        let rows_per_chunk = CHUNK_BYTES.div_ceil(w * core::mem::size_of::<F>());
79        (0..h.div_ceil(rows_per_chunk))
80            .into_par_iter()
81            .for_each(|chunk| {
82                let start = chunk * rows_per_chunk;
83                let end = start + rows_per_chunk.min(h - start);
84                // Keep Rayon jobs, including one-worker pools, free of a helper call per row pair.
85                for i in start..end {
86                    swap(i, !PARALLEL_ENABLED);
87                }
88            });
89    }
90}
91
92/// Swap two rows `i` and `j` in a [`RowMajorMatrix`].
93///
94/// # Panics
95/// Panics if the indices are out of bounds or not ordered as `i < j`.
96///
97/// # Arguments
98/// - `mat`: The matrix to modify.
99/// - `i`: The first row index (must be less than `j`).
100/// - `j`: The second row index.
101pub fn swap_rows<F: Clone + Send + Sync>(mat: &mut RowMajorMatrix<F>, i: usize, j: usize) {
102    let w = mat.width();
103    let (upper, lower) = mat.values.split_at_mut(j * w);
104    let row_i = &mut upper[i * w..(i + 1) * w];
105    let row_j = &mut lower[..w];
106    row_i.swap_with_slice(row_j);
107}
108
109/// Swap two rows `i` and `j` in-place using raw pointer access.
110///
111/// This function is equivalent to [`swap_rows`] but uses unsafe raw pointer math for better performance.
112///
113/// # Safety
114/// - The caller must ensure `i < j < h`, where `h` is the height of the matrix.
115/// - The pointer must point to a vector corresponding to a matrix of width `w`.
116///
117/// # Arguments
118/// - `mat`: A mutable pointer to the underlying matrix data.
119/// - `w`: The matrix width (number of columns).
120/// - `i`: The first row index.
121/// - `j`: The second row index.
122/// - `use_outline`: Whether wide row swaps may use the separate slice helper.
123unsafe fn swap_rows_raw<F>(mat: *mut F, w: usize, i: usize, j: usize, use_outline: bool) {
124    unsafe {
125        let row_i = core::slice::from_raw_parts_mut(mat.add(i * w), w);
126        let row_j = core::slice::from_raw_parts_mut(mat.add(j * w), w);
127        // Row bytes: amortize the function call while keeping small swaps inline.
128        if use_outline && core::mem::size_of_val(row_i) >= 64 {
129            swap_row_slices(row_i, row_j);
130        } else {
131            row_i.swap_with_slice(row_j);
132        }
133    }
134}
135
136// Keep disjoint row slices at a function boundary so the swap loop retains noalias information.
137#[inline(never)]
138fn swap_row_slices<F>(row_i: &mut [F], row_j: &mut [F]) {
139    row_i.swap_with_slice(row_j);
140}
141
142#[cfg(test)]
143mod tests {
144    use alloc::vec;
145
146    use super::*;
147    use crate::dense::RowMajorMatrix;
148
149    #[test]
150    fn test_swap_rows_basic() {
151        let mut matrix = RowMajorMatrix::new(
152            vec![
153                1, 2, 3, // row 0
154                4, 5, 6, // row 1
155                7, 8, 9, // row 2
156                10, 11, 12, // row 3
157            ],
158            3,
159        );
160
161        // Swap rows 0 and 2
162        swap_rows(&mut matrix, 0, 2);
163
164        assert_eq!(
165            matrix.values,
166            vec![
167                7, 8, 9, // row 0 (was row 2)
168                4, 5, 6, // row 1 (unchanged)
169                1, 2, 3, // row 2 (was row 0)
170                10, 11, 12, // row 3 (unchanged)
171            ]
172        );
173    }
174
175    #[test]
176    fn test_swap_rows_raw_basic() {
177        let mut matrix = RowMajorMatrix::new(
178            vec![
179                1, 2, 3, // row 0
180                4, 5, 6, // row 1
181                7, 8, 9, // row 2
182            ],
183            3,
184        );
185        let ptr = matrix.values.as_mut_ptr();
186        unsafe {
187            swap_rows_raw(ptr, matrix.width(), 0, 2, false);
188        }
189
190        assert_eq!(
191            matrix.values,
192            vec![
193                7, 8, 9, // row 0 (was row 2)
194                4, 5, 6, // row 1 (unchanged)
195                1, 2, 3, // row 2 (was row 0)
196            ]
197        );
198    }
199
200    #[test]
201    fn test_reverse_matrix_index_bits_pow2_height() {
202        let mut matrix = RowMajorMatrix::new(
203            vec![
204                0, 1, // row 0
205                2, 3, // row 1
206                4, 5, // row 2
207                6, 7, // row 3
208                8, 9, // row 4
209                10, 11, // row 5
210                12, 13, // row 6
211                14, 15, // row 7
212            ],
213            2,
214        );
215
216        reverse_matrix_index_bits(&mut matrix);
217
218        assert_eq!(
219            matrix.values,
220            vec![
221                0, 1, // row 0 → index 0b000 → stays at 0
222                8, 9, // row 1 → index 0b001 → goes to index 4
223                4, 5, // row 2 → index 0b010 → stays
224                12, 13, // row 3 → index 0b011 → goes to index 6
225                2, 3, // row 4 → index 0b100 → was row 1
226                10, 11, // row 5 → index 0b101 → stays
227                6, 7, // row 6 → index 0b110 → was row 3
228                14, 15, // row 7 → index 0b111 → stays
229            ]
230        );
231    }
232
233    #[test]
234    fn test_reverse_matrix_index_bits_strings() {
235        use alloc::string::ToString;
236        use alloc::vec::Vec;
237
238        for width in [1, 3, 17] {
239            for log_h in 0..=14 {
240                let height = 1 << log_h;
241                let original: Vec<_> = (0..height * width).map(|i| i.to_string()).collect();
242                let expected: Vec<_> = (0..height)
243                    .flat_map(|row| {
244                        let start = reverse_bits_len(row, log_h) * width;
245                        original[start..start + width].iter().cloned()
246                    })
247                    .collect();
248                let mut matrix = RowMajorMatrix::new(original.clone(), width);
249                reverse_matrix_index_bits(&mut matrix);
250                assert_eq!(matrix.values, expected, "width={width}, log_h={log_h}");
251                reverse_matrix_index_bits(&mut matrix);
252                assert_eq!(
253                    matrix.values, original,
254                    "involution: width={width}, log_h={log_h}"
255                );
256            }
257        }
258    }
259
260    #[test]
261    fn test_reverse_matrix_index_bits_preserves_owners_without_cloning() {
262        use alloc::vec::Vec;
263        use core::sync::atomic::{AtomicUsize, Ordering};
264
265        // Fits the width-one fast path while detecting cloning and lost or duplicate owners.
266        struct Owner<'a>(&'a AtomicUsize);
267        impl Clone for Owner<'_> {
268            fn clone(&self) -> Self {
269                panic!("bit reversal must not clone elements");
270            }
271        }
272        impl Drop for Owner<'_> {
273            fn drop(&mut self) {
274                self.0.fetch_add(1, Ordering::Relaxed);
275            }
276        }
277
278        for width in [1, 3, 17] {
279            for log_h in 0..=15 {
280                let height = 1 << log_h;
281                let drops: Vec<_> = (0..height * width).map(|_| AtomicUsize::new(0)).collect();
282                let mut matrix = RowMajorMatrix::new(drops.iter().map(Owner).collect(), width);
283                reverse_matrix_index_bits(&mut matrix);
284                for (i, value) in matrix.values.iter().enumerate() {
285                    let source = reverse_bits_len(i / width, log_h) * width + i % width;
286                    assert!(core::ptr::eq(value.0, &drops[source]));
287                }
288                reverse_matrix_index_bits(&mut matrix);
289                for (value, count) in matrix.values.iter().zip(&drops) {
290                    assert!(core::ptr::eq(value.0, count));
291                    assert_eq!(count.load(Ordering::Relaxed), 0);
292                }
293                drop(matrix);
294                assert!(drops.iter().all(|count| count.load(Ordering::Relaxed) == 1));
295            }
296        }
297    }
298
299    #[test]
300    fn test_reverse_matrix_index_bits_work_unit_boundaries() {
301        use alloc::vec::Vec;
302
303        // An uneven final chunk, then rows just below, at, and above 64 KiB.
304        for (width, height) in [(3, 8192), (16_383, 8), (16_384, 8), (16_385, 8)] {
305            let original: Vec<u32> = (0..width * height).map(|i| i as u32).collect();
306            let mut matrix = RowMajorMatrix::new(original.clone(), width);
307
308            reverse_matrix_index_bits(&mut matrix.as_view_mut());
309            for (row, values) in matrix.values.chunks_exact(width).enumerate() {
310                let source = row.reverse_bits() >> (usize::BITS - height.ilog2());
311                assert_eq!(
312                    values,
313                    &original[source * width..(source + 1) * width],
314                    "width={width}, height={height}, row={row}"
315                );
316            }
317            reverse_matrix_index_bits(&mut matrix);
318            assert_eq!(matrix.values, original, "width={width}, height={height}");
319        }
320    }
321
322    #[test]
323    fn test_reverse_matrix_index_bits_zero_sized_elements() {
324        let mut matrix = RowMajorMatrix::new(vec![(); 3 * 8192], 3);
325        reverse_matrix_index_bits(&mut matrix);
326        assert_eq!(matrix.width(), 3);
327        assert_eq!(matrix.height(), 8192);
328        assert_eq!(matrix.values.len(), 3 * 8192);
329    }
330
331    #[test]
332    fn test_reverse_matrix_index_bits_height_1() {
333        let mut matrix = RowMajorMatrix::new(
334            vec![
335                42, 43, // row 0
336            ],
337            2,
338        );
339
340        // Bit-reversing a height-1 matrix should do nothing.
341        reverse_matrix_index_bits(&mut matrix);
342
343        assert_eq!(
344            matrix.values,
345            vec![
346                42, 43, // row 0 (unchanged)
347            ]
348        );
349    }
350
351    #[test]
352    #[should_panic]
353    fn test_reverse_matrix_index_bits_non_power_of_two_should_panic() {
354        // height = 3 → not a power of two → should panic
355        let mut matrix = RowMajorMatrix::new(
356            vec![
357                1, 2, // row 0
358                3, 4, // row 1
359                5, 6, // row 2
360            ],
361            2,
362        );
363
364        reverse_matrix_index_bits(&mut matrix);
365    }
366}