Skip to main content

rustymath_core/reduce/
pieces.rs

1use super::*;
2
3/// A block of an array that [`fold_pieces`] folds, as its producer hands it over: the block's items, or the reduction's total of its `n` items, where the items all lie in one lane along the last axis.
4pub enum Part<'a, T, S> { Items(&'a [T]), Total(usize, S) }
5
6/// Folds an array of shape `[outer, len, inner]` by `R` into `totals`, the folds of its lanes along the middle axis, and gives whether every fold fits. `run(range, fold)` produces the array's items in `range` in order, and hands each block of them to `fold` with the position of its first item, as a [`Part`]: the items, or their total. The array folds in one piece for each of this thread's [`parallel::threads`], each of at least [`piece_size`] items. On one thread it folds as one piece. A lane that one piece holds folds as [`fold_into`] folds it, and a block's total joins it as [`fold_total`] joins it. A lane that crosses pieces folds its part in each piece, and those folds merge.
7pub fn fold_pieces<T: Copy, R: Reduction<T>>(
8    totals: &mut [R::Total], dims: [usize; 3], run: &(dyn Fn(Range<usize>, &mut dyn FnMut(usize, Part<'_, T, R::Total>)) + Sync)) -> bool {
9    let [outer, len, inner] = dims;
10    // A unit is a lane along the last axis, or a group of `inner` lanes along another axis.
11    let (n, unit, least) = (outer * len * inner, len * inner, piece_size(inner));
12    let size = n.div_ceil((n / least).clamp(1, parallel::threads())).max(least).next_multiple_of(CHUNK);
13    let mut pieces = Vec::with_capacity(n.div_ceil(size));
14    let (mut rest, mut taken) = (&mut *totals, 0);
15    for start in (0..n).step_by(size) {
16        let end = n.min(start + size);
17        let (lo, hi) = (start.div_ceil(unit), (end / unit).max(start.div_ceil(unit)));
18        let (_, after) = std::mem::take(&mut rest).split_at_mut((lo - taken) * inner);
19        let (own, after) = after.split_at_mut((hi - lo) * inner);
20        (rest, taken) = (after, hi);
21        let (front, back) = (end.min(lo * unit), end.min(lo * unit).max(hi * unit));
22        let partial = |at: usize| (at / unit, vec![R::identity(); inner]);
23        pieces.push(Piece { range: start..end, lo, hi, own, front: (start < front).then(|| partial(start)), back: (back < end).then(|| partial(back)) });
24    }
25    let mut fits = parallel::pieces_of(&mut pieces, 1, usize::MAX, &|_, pieces| pieces.iter_mut().fold(true, |ok, piece| ok & piece.fold::<T, R>(dims, run)));
26    let partials: Vec<_> = pieces.into_iter().flat_map(|piece| [piece.front, piece.back]).flatten().collect();
27    let mut last = usize::MAX;
28    for (u, part) in partials {
29        let folds = &mut totals[u * inner..(u + 1) * inner];
30        if u == last { fits &= merges_of::<T, R>(folds, &part); } else {
31            folds.copy_from_slice(&part);
32            last = u;
33        }
34    }
35    fits
36}
37
38/// One piece of [`fold_pieces`]: its items, the units `lo..hi` that it holds whole with their folds, and the partial folds of the units that it shares with the pieces before and after it, each with its unit. A partial fold starts at `R::identity()`.
39struct Piece<'a, S> { range: Range<usize>, lo: usize, hi: usize, own: &'a mut [S], front: Option<(usize, Vec<S>)>, back: Option<(usize, Vec<S>)> }
40
41impl<S: Copy> Piece<'_, S> {
42    /// Produces the piece's items through `run`, folds each block into the piece's own folds or its partial folds, and gives whether every fold fits.
43    fn fold<T: Copy, R: Reduction<T, Total = S>>(
44        &mut self, dims: [usize; 3], run: &(dyn Fn(Range<usize>, &mut dyn FnMut(usize, Part<'_, T, S>)) + Sync)) -> bool {
45        let (unit, mut fits) = (dims[1] * dims[2], true);
46        run(self.range.clone(), &mut |mut start, part| match part {
47            Part::Items(mut items) => {
48                while !items.is_empty() {
49                    let (folds, at, end) = self.place(start, unit);
50                    let (block, after) = items.split_at((end - start).min(items.len()));
51                    fits &= fold_into::<T, R>(folds, dims, at, block);
52                    (start, items) = (start + block.len(), after);
53                }
54            }
55            Part::Total(n, total) => {
56                let (folds, at, end) = self.place(start, unit);
57                assert!(start + n <= end, "a block's total joins the folds of one part of a piece");
58                fits &= fold_total::<T, R>(folds, dims, at, n, total);
59            }
60        });
61        fits
62    }
63
64    /// The folds that the items from position `start` join: the partial folds of the unit that the piece shares with the piece before it, its own folds, or the partial folds of the unit that it shares with the piece after it. With them come the position of `start` among those folds' items, and the position where the items that they take end.
65    fn place(&mut self, start: usize, unit: usize) -> (&mut [S], usize, usize) {
66        let (a, b) = (self.lo * unit, self.hi * unit);
67        match (start < a, start < b) {
68            (true, _) => self.front.as_mut().map(|(u, f)| (&mut f[..], start - *u * unit, a)).expect("a piece that starts inside a unit has a front fold"),
69            (_, true) => (&mut *self.own, start - a, b),
70            _ => self.back.as_mut().map(|(u, f)| (&mut f[..], start - *u * unit, usize::MAX)).expect("a piece that ends inside a unit has a back fold"),
71        }
72    }
73}
74
75/// Joins `total`, the fold of the `n` items from position `start` of an array of shape `[outer, len, inner]` held row by row, into `totals`, the folds by `R` of its lanes along the middle axis so far, as [`fold_into`] joins those items. The items must all lie in one lane along the last axis. Gives whether the fold still fits.
76fn fold_total<T, R: Reduction<T>>(totals: &mut [R::Total], [_, len, inner]: [usize; 3], start: usize, n: usize, total: R::Total) -> bool {
77    assert!(inner == 1 && start % len + n <= len, "a block's total covers items of one lane along the last axis");
78    let (i, at) = (start / len, start % len);
79    if at == 0 { totals[i] = total; return true; }
80    let (t, fits) = R::merge(totals[i], total);
81    totals[i] = t;
82    fits
83}
84
85/// Folds lanes `first..first + totals.len()` along the middle axis of `data`, an array of shape `[_, len, inner]` held row by row, into `totals`, and gives whether every fold fits. Each lane folds whole. A lane along the last axis folds as [`fold_rows`] folds it. Along another axis, a whole group of `inner` lanes folds its rows as [`step_rows`] folds them, and part of a group folds its columns as [`columns_by`] folds them.
86pub(super) fn lanes<T: Copy, R: Reduction<T>>(totals: &mut [R::Total], first: usize, data: &[T], [_, len, inner]: [usize; 3]) -> bool {
87    if inner == 1 { return R::rows(&data[first * len..(first + totals.len()) * len], len, totals); }
88    let (mut rest, mut k, mut fits) = (totals, first, true);
89    let streamed = size_of_val(data) > STREAMED_BYTES;
90    while !rest.is_empty() {
91        let (i, c) = (k / inner, k % inner);
92        let n = (inner - c).min(rest.len());
93        let (folds, after) = std::mem::take(&mut rest).split_at_mut(n);
94        let group = &data[i * len * inner + c..(i + 1) * len * inner];
95        fits &= if folds.len() == inner { step_rows::<T, R>(folds, group, true, streamed) } else { columns_by::<T, R>(folds, group, inner, true, streamed) };
96        (rest, k) = (after, k + folds.len());
97    }
98    fits
99}
100
101/// Steps each of `totals` by the matching item of `row`, and gives whether every fold fits. In a lane's first row, each fold is the fold of its item alone.
102#[inline(always)]
103fn step_row<T: Copy, R: Reduction<T>>(totals: &mut [R::Total], row: &[T], first: bool) -> bool {
104    if !first { return steps_of::<T, R>(totals, row); }
105    let mut ok = true;
106    for (t, &x) in totals.iter_mut().zip(row) { *t = one_of::<T, R>(x, &mut ok) }
107    ok
108}
109
110/// Steps `totals` by every row of `rows`, which hold `totals.len()` items each, and gives whether every fold fits. Where `first` is true, the first row starts each fold, as in [`step_row`]. Narrow rows fold a block of whole rows at a time, into one running total for each item of the block. Each column's running totals then merge.
111#[inline(always)]
112fn step_rows<T: Copy, R: Reduction<T>>(totals: &mut [R::Total], rows: &[T], first: bool, streamed: bool) -> bool {
113    if !R::COMMUTATIVE { return rows_by::<T, R, 1>(totals, rows, first, streamed); }
114    match crate::slice::running(size_of::<R::Total>()) {
115        8 => rows_by::<T, R, 8>(totals, rows, first, streamed), 16 => rows_by::<T, R, 16>(totals, rows, first, streamed),
116        _ => rows_by::<T, R, 32>(totals, rows, first, streamed)
117    }
118}
119
120/// `step_rows` with up to `N` running totals. Rows wider than `N` fold as [`columns_by`] folds them. Too few rows to fill two blocks, and lanes of fewer than 4 rows, step one row at a time. Merging the columns of so few blocks cost more than the steps it saved, 13 instructions an item for the lanes of 2 rows that 2×2 pooling folds. A block is `N` items where `inner` divides `N`, and three quarters of `N` for rows of 3.
121#[inline(always)]
122fn rows_by<T: Copy, R: Reduction<T>, const N: usize>(totals: &mut [R::Total], rows: &[T], first: bool, streamed: bool) -> bool {
123    let inner = totals.len();
124    if inner > N { return columns_by::<T, R>(totals, rows, inner, first, streamed); }
125    if rows.len() < 2 * N || rows.len() < 4 * inner {
126        return rows.chunks_exact(inner).enumerate().fold(true, |fits, (j, row)| fits & step_row::<T, R>(totals, row, first && j == 0));
127    }
128    match inner {
129        _ if N % inner == 0 => blocks_by::<T, R, N>(totals, rows, first, N), 3 => blocks_by::<T, R, N>(totals, rows, first, 3 * (N / 4)),
130        _ => blocks_by::<T, R, N>(totals, rows, first, N - N % inner)
131    }
132}
133
134/// The bytes of totals that [`columns_by`] steps in a block. They stay in the first-level cache while each row's items stream past. Blocks of 64 KB folded arrays of 64 to 256 very wide rows 20 to 25% faster than blocks of 16 KB on an M5 Max.
135const COLUMN_BYTES: usize = 64 << 10;
136/// The bytes of an array past which [`columns_by`] steps one row at a time: such an array streams from memory, where one stream of rows reads faster than four interleaved ones. On an M5 Max, one thread's `+⌿` of 1,000 f64 columns took 17% less time one row at a time at 64 MB and 6% less at 48 MB, and 5 to 41% more at 32 MB and below. On 4 threads, which share memory's bandwidth, both orders took the same time from 48 to 128 MB (31 October 2026).
137const STREAMED_BYTES: usize = 40 << 20;
138
139/// Steps `totals` by every row of `rows`, as [`step_rows`] does, where `totals` are the folds of the first `totals.len()` columns of rows of `inner` items. `rows` starts at the first of those columns, and its last row may stop after them, as where [`lanes`] folds part of a group's columns. `step_rows` folds rows wider than `slice::running`'s count of running totals this way. Each block of [`COLUMN_BYTES`] of `totals` steps by its columns of four rows at a time, so each row's items are read in the order they lie in memory. Where `streamed` is true, for an array past [`STREAMED_BYTES`], each block steps one row at a time. Strips of 32 columns, each folded down every row in registers, read their items a row apart, and on arrays larger than the cache took about 4 to 6 times as long as a fold along the last axis (27 October 2026).
140fn columns_by<T: Copy, R: Reduction<T>>(totals: &mut [R::Total], rows: &[T], inner: usize, first: bool, streamed: bool) -> bool {
141    let (len, width) = (rows.len().div_ceil(inner), (COLUMN_BYTES / size_of::<R::Total>()).max(1));
142    totals.chunks_mut(width).enumerate().fold(true, |mut fits, (b, block)| {
143        let (at, w) = (b * width, block.len());
144        let row = |j: usize| &rows[j * inner + at..j * inner + at + w];
145        let mut j = usize::from(first && len > 0);
146        if j == 1 { fits &= step_row::<T, R>(block, row(0), true); }
147        while !streamed && j + 4 <= len {
148            fits &= steps_of4::<T, R>(block, [row(j), row(j + 1), row(j + 2), row(j + 3)]);
149            j += 4;
150        }
151        (j..len).fold(fits, |fits, j| fits & steps_of::<T, R>(block, row(j)))
152    })
153}
154
155/// Steps each of `totals` by the matching item of each of `rows` in turn, as four calls of [`steps_of`] would, in one pass.
156#[inline(always)]
157fn steps_of4<T: Copy, R: Reduction<T>>(totals: &mut [R::Total], rows: [&[T]; 4]) -> bool {
158    let n = totals.len();
159    let ([a, b, c, d], mut fits) = (rows.map(|r| &r[..n]), true);
160    for k in 0..n {
161        let (t, f) = R::checked(totals[k], a[k]);
162        let (t, g) = R::checked(t, b[k]);
163        let (t, h) = R::checked(t, c[k]);
164        let (t, i) = R::checked(t, d[k]);
165        (totals[k], fits) = (t, fits & f & g & h & i);
166    }
167    fits
168}
169
170/// `rows_by` in blocks of `width` items, a multiple of `inner` no greater than `N`. A width the compiler knows keeps the running totals in registers.
171#[inline(always)]
172fn blocks_by<T: Copy, R: Reduction<T>, const N: usize>(totals: &mut [R::Total], rows: &[T], first: bool, width: usize) -> bool {
173    let inner = totals.len();
174    let (blocks, rest) = rows.split_at(rows.len() - rows.len() % width);
175    // `run` can stay in registers through the loop once `steps_of` inlines, unlike `all`.
176    let (mut run, mut fits) = ([R::identity(); N], true);
177    for block in blocks.chunks_exact(width) { fits &= steps_of::<T, R>(&mut run[..width], block) }
178    let mut all = run;
179    let parts = &mut all[..width];
180    fits &= steps_of::<T, R>(parts, rest);
181    for (c, total) in totals.iter_mut().enumerate() {
182        let (column, ok) = parts[c..].iter().step_by(inner).fold((R::identity(), true), |(s, ok), &p| {
183            let (t, f) = R::merge(s, p);
184            (t, ok & f)
185        });
186        let (t, f) = if first { (column, ok) } else { let (t, f) = R::merge(*total, column); (t, f & ok) };
187        (*total, fits) = (t, fits & f);
188    }
189    fits
190}
191
192/// Folds `block`, the items from position `start` on of an array of shape `[outer, len, inner]` held row by row, into `totals`, the folds by `R` of the array's lanes along its middle axis so far, which start at `R::unit()`. Gives whether every fold still fits. Along the last axis, a lane's items are consecutive. Whole lanes fold as `fold_rows` folds them, and the pieces of a lane that a block boundary splits fold by `slice` and join by `merge`. Along any other axis, runs of whole rows fold as [`step_rows`] folds them, and a row that a block boundary splits steps item by item.
193pub fn fold_into<T: Copy, R: Reduction<T>>(totals: &mut [R::Total], [outer, len, inner]: [usize; 3], start: usize, block: &[T]) -> bool {
194    let (mut rest, mut fits) = (block, true);
195    if rest.is_empty() { return true; }
196    if inner == 1 {
197        let (mut i, at) = (start / len, start % len);
198        // The rest of a lane that an earlier block started.
199        if at > 0 {
200            let (piece, after) = rest.split_at((len - at).min(rest.len()));
201            let (s, f) = R::slice(piece);
202            let (t, g) = R::merge(totals[i], s);
203            (totals[i], fits) = (t, f & g);
204            if at + piece.len() < len { return fits; }
205            (rest, i) = (after, i + 1);
206        }
207        let count = rest.len() / len;
208        let (whole, started) = rest.split_at(count * len);
209        fits &= R::rows(whole, len, &mut totals[i..i + count]);
210        // The start of a lane that a later block finishes.
211        if !started.is_empty() {
212            let (s, f) = R::slice(started);
213            (totals[i + count], fits) = (s, fits & f);
214        }
215    } else {
216        let (row, streamed) = (start / inner, outer * len * inner * size_of::<T>() > STREAMED_BYTES);
217        let (mut i, mut j, mut k) = (row / len, row % len, start % inner);
218        while !rest.is_empty() {
219            let rows = if k == 0 { (len - j).min(rest.len() / inner) } else { 0 };
220            if rows > 0 {
221                let (whole, after) = rest.split_at(rows * inner);
222                fits &= step_rows::<T, R>(&mut totals[i * inner..(i + 1) * inner], whole, j == 0, streamed);
223                (rest, j) = (after, j + rows);
224            } else {
225                let (piece, after) = rest.split_at((inner - k).min(rest.len()));
226                fits &= step_row::<T, R>(&mut totals[i * inner + k..], piece, j == 0);
227                (rest, k) = (after, k + piece.len());
228                if k == inner { (j, k) = (j + 1, 0); }
229            }
230            if j == len { (i, j) = (i + 1, 0); }
231        }
232    }
233    fits
234}
235
236/// Folds each row of `data`, a matrix with `columns` items in each row, by `R` into the matching item of `out`, and gives whether every fold fits. A row of fewer than 16 items folds by `checked`, with a loop of its own for each such length, such as an image's 3 channels. A loop of unknown length cost about 2 ns for each row. A longer row folds by `slice`, at the cost of a call and some setup for each row, which `running_results` keeps small for a short row.
237#[inline]
238pub(super) fn fold_rows<T: Copy, R: Reduction<T>>(data: &[T], columns: usize, out: &mut [R::Total]) -> bool {
239    #[inline(always)]
240    fn each<const K: usize, T: Copy, R: Reduction<T>>(data: &[T], out: &mut [R::Total]) -> bool {
241        let mut fits = true;
242        for (o, row) in out.iter_mut().zip(data.as_chunks::<K>().0) {
243            let (t, f) = row[1..].iter().fold(R::one(row[0]), |(s, ok), &x| {
244                let (t, f) = R::checked(s, x);
245                (t, ok & f)
246            });
247            (*o, fits) = (t, fits & f);
248        }
249        fits
250    }
251    macro_rules! short {
252        ($($k:literal)+) => {
253            match columns {
254                $($k => each::<$k, T, R>(data, out),)+
255                _ => out.iter_mut().zip(data.chunks_exact(columns)).fold(true, |fits, (o, row)| { let (t, f) = R::slice(row); *o = t; fits & f }),
256            }
257        };
258    }
259    short!(1 2 3 4 5 6 7 8 9 10 11 12 13 14 15)
260}
261
262/// [`Reduction::rows`] for a reduction whose step is one instruction: a row of 16 to fewer than [`SHORT_LANE`] items folds by `checked`, [`LANES`] rows side by side, and any other row as [`fold_rows`] folds it.
263#[inline]
264pub(super) fn rows_side_by_side<T: Copy, R: Reduction<T>>(data: &[T], columns: usize, out: &mut [R::Total]) -> bool {
265    if !(16..SHORT_LANE).contains(&columns) { return fold_rows::<T, R>(data, columns, out); }
266    let whole = out.len() / LANES * LANES;
267    let (head, tail) = out.split_at_mut(whole);
268    side_by_side::<LANES, _, _>(&data[..whole * columns], columns, R::one, R::checked, |_, _| {}, |row, t| head[row] = t)
269        & side_by_side::<1, _, _>(&data[whole * columns..], columns, R::one, R::checked, |_, _| {}, |row, t| tail[row] = t)
270}