Skip to main content

thermite/
sort.rs

1//! Sort order: which direction a comparator points.
2//!
3//! A sorting network is a fixed sequence of compare-exchanges, and a
4//! compare-exchange is fully described by "which of the two values goes to the
5//! lower index". [`SortOrder`] is that choice, as a compile-time parameter, so
6//! one network body serves every direction.
7//!
8//! # The direction flip is free
9//!
10//! Flipping every comparator in an *ascending* network yields a *descending*
11//! one - for any network, not just the bitonic ones here. If `f` is an
12//! order-reversing bijection then `min(f a, f b) = f(max(a, b))`, so a flipped
13//! network `N'` satisfies `N'(x) = f^-1(N(f(x)))`: the same permutation network
14//! run on reversed values, which is `x` sorted descending.
15//!
16//! At the machine level the flip is not even an extra instruction. A network
17//! layer is `permute`, `min`, `max`, `blend`; reversing it swaps which of the
18//! `min`/`max` results feeds the blend's true arm. Measured on znver3 (llvm-mca,
19//! `crates/sortasm` probe), `f32x8`:
20//!
21//! | | insns | `vminps` | `vmaxps` | shuffles | RThroughput | latency |
22//! |---|---|---|---|---|---|---|
23//! | [`Ascending`] | 29 | 6 | 6 | 8 | 9.0 | **43 cyc** |
24//! | [`Descending`] | 29 | 6 | 6 | 8 | 9.0 | **43 cyc** |
25//! | ascending then `reverse` | 31 | 6 | 6 | 11 | 8.5 | 50 cyc |
26//!
27//! So a descending sort costs exactly nothing, where post-processing an
28//! ascending one with [`reverse`](crate::register::Register::reverse) costs 3
29//! shuffles and 16% latency. The counts of `min` and `max` are unchanged in both
30//! directions because each layer issues one of each regardless; only the blend
31//! operands swap.
32//!
33//! # Generic methods, not a generic trait
34//!
35//! The obvious shape parameterizes the trait by the register
36//! (`trait SortOrder<R: NumericRegister>`). It serves native registers fine and
37//! then hard-blocks on the first *delegating* one: inside
38//! `ArrayRegister::<R, N>::sort_by::<O>`, calling `R::sort_by::<O>` needs
39//! `O: SortOrder<R>`, which `O: SortOrder<Self>` does not imply - and the bound
40//! cannot be added, because the method signature is fixed by the trait
41//! declaration, which has no `R` to name. Each nesting level would want another
42//! bound.
43//!
44//! Generic *methods* keep the bound flat: `O: SortOrder` is the whole
45//! requirement at every layer, whatever the register, however deeply nested.
46
47use crate::register::{NumericRegister, Storage};
48use crate::vector::NumericVector;
49
50/// The direction a comparator points, as a compile-time parameter.
51///
52/// Implementors are zero-sized markers ([`Ascending`], [`Descending`]) selected
53/// with turbofish - `R::sort_by::<Descending>(v)`. Both methods must be a
54/// consistent pair: `first` and `last` are the two halves of one
55/// compare-exchange, so for any `a`, `b` the multiset `{first(a,b), last(a,b)}`
56/// must equal `{a, b}`, and `first(a,b)` must not come after `last(a,b)` under
57/// the order. Violating that does not just misorder - it duplicates and drops
58/// values, because a network never re-reads what it overwrote.
59///
60/// Deliberately only these two methods. They are the whole of what a *network*
61/// needs; the padding sentinels (a value that sorts after everything, for
62/// filling a partial register) and the members a partition-based sort would want
63/// (`compare`, `prev_value`) get added when something calls them.
64pub trait SortOrder {
65    /// Whether this order is smallest-first.
66    ///
67    /// **Must agree with [`first`](Self::first) and [`last`](Self::last)** - it
68    /// is the same fact stated a second way, and nothing checks that the two
69    /// statements match.
70    ///
71    /// It exists because the scalar fallback
72    /// ([`sort_any`](crate::backend::generic::polyfills::sort::sort_any), used
73    /// by any register with no network for its lane count) sorts *elements*
74    /// through `PartialOrd`, where the register-level `first`/`last` cannot
75    /// reach. Rather than widen this trait with a scalar comparator pair used by
76    /// one slow path, that path sorts ascending and reverses on this flag. The
77    /// cost is one permute on a body that is already quadratic.
78    const IS_ASCENDING: bool;
79
80    /// The value that belongs at the **lower** index of a comparator pair.
81    ///
82    /// Named for the position rather than for `min`, because the two stop
83    /// coinciding as soon as the order is anything but ascending.
84    fn first<R: NumericRegister>(a: Storage<R>, b: Storage<R>) -> Storage<R>;
85
86    /// The value that belongs at the **higher** index of a comparator pair.
87    fn last<R: NumericRegister>(a: Storage<R>, b: Storage<R>) -> Storage<R>;
88
89    /// [`first`](Self::first) at the vector layer.
90    ///
91    /// A second pair rather than one generic over both layers because
92    /// [`NumericVector`] has no associated register type to route through (only
93    /// its float/signed/unsigned sub-traits do), and `Storage<R>` is a
94    /// projection, so no helper trait can cover both. Keeping the pairs on one
95    /// trait is what stops a marker from meaning ascending at one layer and
96    /// descending at the other.
97    fn vector_first<V: NumericVector>(a: V, b: V) -> V;
98
99    /// [`last`](Self::last) at the vector layer.
100    fn vector_last<V: NumericVector>(a: V, b: V) -> V;
101
102    /// A value that sorts **after** every real input under this order - the
103    /// padding sentinel.
104    ///
105    /// Filling the unused lanes of a partial register with this lets a fixed
106    /// network sort a run shorter than the register: the sentinels sort to the
107    /// tail and are never stored back. Ascending wants the order maximum
108    /// (`+inf` for floats, not `MAX`); descending wants the order minimum, which
109    /// is why this lives on the order rather than on the register.
110    ///
111    /// **NaN is not covered.** No value sorts past NaN because NaN is unordered,
112    /// so a float sort must remove NaN before the network runs - see
113    /// `thermite_sort`. Padding with `+inf` alongside a NaN in the data gives a
114    /// backend-dependent result, since `min`/`max` NaN semantics legitimately
115    /// differ across ISAs (the differential suite carries a `Tol::ExactOrNan`
116    /// for precisely this).
117    #[inline(always)]
118    fn last_value<V: NumericVector>() -> V {
119        V::splat(if const { Self::IS_ASCENDING } {
120            <V::Element as crate::element::Element>::ORDER_MAX
121        } else {
122            <V::Element as crate::element::Element>::ORDER_MIN
123        })
124    }
125
126    /// A value that sorts **before** every real input under this order. The
127    /// mirror of [`last_value`](Self::last_value); same NaN caveat.
128    #[inline(always)]
129    fn first_value<V: NumericVector>() -> V {
130        V::splat(if const { Self::IS_ASCENDING } {
131            <V::Element as crate::element::Element>::ORDER_MIN
132        } else {
133            <V::Element as crate::element::Element>::ORDER_MAX
134        })
135    }
136}
137
138/// Sort smallest-first. The default everywhere a direction is not named.
139pub struct Ascending;
140
141/// Sort largest-first.
142///
143/// Costs exactly the same as [`Ascending`] - see the module docs.
144pub struct Descending;
145
146impl SortOrder for Ascending {
147    const IS_ASCENDING: bool = true;
148
149    #[inline(always)]
150    fn first<R: NumericRegister>(a: Storage<R>, b: Storage<R>) -> Storage<R> {
151        R::min(a, b)
152    }
153
154    #[inline(always)]
155    fn last<R: NumericRegister>(a: Storage<R>, b: Storage<R>) -> Storage<R> {
156        R::max(a, b)
157    }
158
159    #[inline(always)]
160    fn vector_first<V: NumericVector>(a: V, b: V) -> V {
161        a.min(b)
162    }
163
164    #[inline(always)]
165    fn vector_last<V: NumericVector>(a: V, b: V) -> V {
166        a.max(b)
167    }
168}
169
170impl SortOrder for Descending {
171    const IS_ASCENDING: bool = false;
172
173    #[inline(always)]
174    fn first<R: NumericRegister>(a: Storage<R>, b: Storage<R>) -> Storage<R> {
175        R::max(a, b)
176    }
177
178    #[inline(always)]
179    fn last<R: NumericRegister>(a: Storage<R>, b: Storage<R>) -> Storage<R> {
180        R::min(a, b)
181    }
182
183    #[inline(always)]
184    fn vector_first<V: NumericVector>(a: V, b: V) -> V {
185        a.max(b)
186    }
187
188    #[inline(always)]
189    fn vector_last<V: NumericVector>(a: V, b: V) -> V {
190        a.min(b)
191    }
192}
193
194// ---------------------------------------------------------------------------
195// Compare-exchange layer descriptions
196//
197// Pure index math over a lane count - no register, no vector, no layer. Both
198// the register-layer networks (`backend::generic::polyfills::sort`) and the
199// vector-layer block sort (`thermite_sort::merge`) build their layers from
200// these, and there is no reason for two copies of the arithmetic.
201// ---------------------------------------------------------------------------
202
203use crate::register::SwizzleIndices;
204use generic_array::{ArrayLength, GenericArray};
205
206/// Lane `i` faces lane `i ^ K`: the partner set of a distance-`K`
207/// compare-exchange, and the shuffle behind Highway's `SortPairsDistance{K}` /
208/// `SwapAdjacentPairs` / `SwapAdjacentQuads`.
209///
210/// `K >= N` is clamped to the identity rather than left out of range. Such a
211/// stage is dead code that a caller's `if const` ladder never reaches, but its
212/// `INDICES` are still const-evaluated, and an out-of-range swizzle index is
213/// undefined behavior by contract rather than merely unused.
214pub struct XorIdx<const K: usize, N>(core::marker::PhantomData<N>);
215
216impl<const K: usize, N: ArrayLength> SwizzleIndices<N> for XorIdx<K, N> {
217    const INDICES: GenericArray<u32, N> = const {
218        // `GenericArray<u32, N>` has no const literal constructor for a generic
219        // `N`, so zero-initialize (all-zero `u32` is valid) and fill in place.
220        let mut idxs: GenericArray<u32, N> = unsafe { core::mem::zeroed() };
221        let ptr = &mut idxs as *mut GenericArray<u32, N> as *mut u32;
222
223        let k = if K < N::USIZE { K } else { 0 };
224
225        let mut i = 0;
226        while i < N::USIZE {
227            unsafe { *ptr.add(i) = (i ^ k) as u32 };
228            i += 1;
229        }
230        idxs
231    };
232}
233
234/// Lane `i` faces its mirror within its contiguous group of `K` lanes - the
235/// shuffle behind Highway's `ReverseKeys{K}` and `SortPairsReverse{K}`.
236///
237/// `K > N` is clamped to a full reverse; see [`XorIdx`] for why that matters.
238pub struct RevIdx<const K: usize, N>(core::marker::PhantomData<N>);
239
240impl<const K: usize, N: ArrayLength> SwizzleIndices<N> for RevIdx<K, N> {
241    const INDICES: GenericArray<u32, N> = const {
242        let mut idxs: GenericArray<u32, N> = unsafe { core::mem::zeroed() };
243        let ptr = &mut idxs as *mut GenericArray<u32, N> as *mut u32;
244
245        let k = if K < N::USIZE { K } else { N::USIZE };
246
247        let mut i = 0;
248        while i < N::USIZE {
249            unsafe { *ptr.add(i) = ((i & !(k - 1)) | (k - 1 - (i & (k - 1)))) as u32 };
250            i += 1;
251        }
252        idxs
253    };
254}
255
256/// Lanes whose index has `bit` set - the lanes that receive
257/// [`SortOrder::last`] of their pair. `bit == 0` means no lane does.
258///
259/// Bits at or above the lane count are ignored by
260/// [`from_native_bitmask`](crate::mask::GenericMask::from_native_bitmask), so
261/// one 64-bit constant serves every width.
262pub const fn keep_bits(bit: usize) -> u64 {
263    let mut mask = 0u64;
264    if bit == 0 {
265        return mask;
266    }
267    let mut i = 0;
268    while i < 64 {
269        if i & bit != 0 {
270            mask |= 1 << i;
271        }
272        i += 1;
273    }
274    mask
275}
276
277/// One in-register compare-exchange layer: a partner permutation, plus the lane
278/// mask saying which side of each pair keeps the later element.
279///
280/// A network layer is `permute` + `first` + `last` + `blend`, and this is the
281/// half of it that varies. Splitting it out as a type rather than two const
282/// parameters is what lets [`RevPairs`] derive its mask from `K / 2` - a const
283/// *expression*, which is stable, where a const-generic expression is not.
284pub trait PairStage<N: ArrayLength> {
285    /// Where each lane finds its partner.
286    type Indices: SwizzleIndices<N>;
287
288    /// Bit `i` set means lane `i` receives [`SortOrder::last`] of its pair.
289    const KEEP_LAST: u64;
290}
291
292/// Compare-exchange lane `i` with lane `i ^ K` (Highway's
293/// `SortPairsDistance{K}`). The high lane of each pair keeps the last.
294///
295/// The halving strides `LANES/2 .. 1` of this are exactly a bitonic cleanup.
296pub struct Distance<const K: usize>;
297
298impl<const K: usize, N: ArrayLength> PairStage<N> for Distance<K> {
299    type Indices = XorIdx<K, N>;
300    const KEEP_LAST: u64 = keep_bits(K);
301}
302
303/// Compare-exchange each lane with its mirror in its group of `K` (Highway's
304/// `SortPairsReverse{K}`).
305///
306/// The upper *half* of each reversed group keeps the last, so the blend mask is
307/// the distance-`K/2` one. That identity is what collapses the two families of
308/// odd-even blend (`OddEvenKeys` / `OddEvenPairs` / `OddEvenQuads`) onto one
309/// [`keep_bits`].
310pub struct RevPairs<const K: usize>;
311
312impl<const K: usize, N: ArrayLength> PairStage<N> for RevPairs<K> {
313    type Indices = RevIdx<K, N>;
314    const KEEP_LAST: u64 = keep_bits(K / 2);
315}
316
317// ---------------------------------------------------------------------------
318// Key-driven lane sorts at the vector layer
319//
320// The register-layer networks move *elements* through `min`/`max`, which is
321// only correct when a lane is one value. A composite vector (autodiff dual,
322// compensated pair, complex) is several component vectors whose lanes move as
323// a unit under a KEY comparison - the primal for `Dual`, the value for
324// `Compensated`, lexicographic (re, im) for `Complex`. These run the exact
325// same stage sequence (`RevPairs`/`Distance`, the depth-minimal construction
326// behind `sort_lanes`), but each compare-exchange derives one *routing mask*
327// from the key and applies it to every component with a single whole-vector
328// `select` - which the composite's own `Mask::select` already does
329// componentwise.
330//
331// The alternative - computing the sorting permutation of the key lanes and
332// `permute`-ing every component once (an argsort) - costs fewer ops when the
333// component count is large, since the network then only carries (key, index).
334// At the component counts that exist today (2-5) the direct form is at parity
335// or better and needs no index machinery; revisit if a many-component
336// composite shows up hot.
337// ---------------------------------------------------------------------------
338
339use crate::mask::GenericMask;
340use crate::vector::GenericVector;
341
342/// The comparison a key-driven lane sort routes on: `key_lt(a, b)` masks the
343/// lanes of `a` strictly before those of `b` under the type's sort key.
344///
345/// A static trait method rather than an `F: Fn` parameter ON PURPOSE. The
346/// closure/fn-item form was measured leaving the comparison out of line - the
347/// `Fn::call` shim through `&F` survived to codegen for the ternlog-carrying
348/// lexicographic comparisons, one base-ISA `call` per network stage - and an
349/// `#[inline(always)]` on a static method cannot be declined that way.
350///
351/// Requirements: a *strict* order test on the key alone (never the payload
352/// components), consistent across lanes. Typically implemented by the
353/// composite for itself (`impl SortKey<Self> for Self`) as its `cmp_lt` or a
354/// component's `cmp_lt`.
355pub trait SortKey<V: GenericVector> {
356    /// Which lanes of `a` sort strictly before those of `b`, by the key.
357    fn key_lt(a: V, b: V) -> V::Mask;
358}
359
360/// One key-driven compare-exchange layer: partner permutation, two strict key
361/// comparisons, one routing select applied to the whole vector.
362///
363/// **Both sides of a pair need their own strict test.** With one mask `m =
364/// lt(partner, self)` and the keep-last side taking `!m`, a key *tie* routes
365/// the same composite to both lanes - one value duplicated, its partner
366/// dropped, invisibly to any test whose payloads tie too. Two strict tests
367/// (`lt(partner, self)` for keep-first lanes, `lt(self, partner)` for
368/// keep-last) make every tied pair keep itself on both sides, so the layer is
369/// a permutation for every input.
370#[inline(always)]
371fn key_stage<V, O, S, K>(v: V) -> V
372where
373    V: GenericVector + crate::swizzle::Swizzle<V::Lanes>,
374    O: SortOrder,
375    S: PairStage<V::Lanes>,
376    K: SortKey<V>,
377{
378    let partner = v.permutev_const::<S::Indices>();
379
380    let (m_first, m_last) = if const { O::IS_ASCENDING } {
381        (K::key_lt(partner, v), K::key_lt(v, partner))
382    } else {
383        (K::key_lt(v, partner), K::key_lt(partner, v))
384    };
385
386    // A constant bitmask, so this folds to a materialized mask constant.
387    let keep_last = V::Mask::from_native_bitmask(S::KEEP_LAST);
388    let take_partner = (m_first & !keep_last) | (m_last & keep_last);
389    take_partner.select(partner, v)
390}
391
392/// Sort the lanes of `v` in `O` order of `K`'s key. Whole lanes move
393/// together: every component of a composite follows its key.
394///
395/// Same depth-minimal construction as the register-layer `sort_lanes` (the
396/// widening tails at one chunk); covers power-of-two lane counts up to 16.
397/// Ties by key keep the input's lane order within each compare-exchange (see
398/// `key_stage`) but the sort as a whole is not stable.
399#[inline(always)]
400pub fn sort_lanes_by_key<V, O, K>(v: V) -> V
401where
402    V: GenericVector + crate::swizzle::Swizzle<V::Lanes>,
403    O: SortOrder,
404    K: SortKey<V>,
405{
406    // NOT a const assert: an `if const` gate in a caller does not stop this
407    // from being monomorphized for wider vectors (guards do not prevent
408    // monomorphization), so a hard compile-time check would break any caller
409    // that gates and falls back. Callers must gate on `V::LANES <= 16`.
410    debug_assert!(
411        V::LANES <= 16 && V::LANES.is_power_of_two(),
412        "sort_lanes_by_key covers power-of-two lane counts up to 16"
413    );
414
415    let v = if const { V::LANES >= 2 } {
416        key_stage::<V, O, RevPairs<2>, K>(v)
417    } else {
418        v
419    };
420    let v = if const { V::LANES >= 4 } {
421        let v = key_stage::<V, O, RevPairs<4>, K>(v);
422        key_stage::<V, O, Distance<1>, K>(v)
423    } else {
424        v
425    };
426    let v = if const { V::LANES >= 8 } {
427        let v = key_stage::<V, O, RevPairs<8>, K>(v);
428        let v = key_stage::<V, O, Distance<2>, K>(v);
429        key_stage::<V, O, Distance<1>, K>(v)
430    } else {
431        v
432    };
433
434    if const { V::LANES >= 16 } {
435        let v = key_stage::<V, O, RevPairs<16>, K>(v);
436        let v = key_stage::<V, O, Distance<4>, K>(v);
437        let v = key_stage::<V, O, Distance<2>, K>(v);
438        key_stage::<V, O, Distance<1>, K>(v)
439    } else {
440        v
441    }
442}
443
444/// [`sort_lanes_by_key`] for an already-**bitonic** vector: the halving
445/// compare-exchange strides `LANES/2 .. 1`. Garbage in, garbage out on
446/// non-bitonic input, exactly like the register-layer `bitonic_clean_lanes`.
447#[inline(always)]
448pub fn bitonic_clean_lanes_by_key<V, O, K>(v: V) -> V
449where
450    V: GenericVector + crate::swizzle::Swizzle<V::Lanes>,
451    O: SortOrder,
452    K: SortKey<V>,
453{
454    // See `sort_lanes_by_key` for why this is not a const assert.
455    debug_assert!(
456        V::LANES <= 16 && V::LANES.is_power_of_two(),
457        "bitonic_clean_lanes_by_key covers power-of-two lane counts up to 16"
458    );
459
460    let v = if const { V::LANES >= 16 } {
461        key_stage::<V, O, Distance<8>, K>(v)
462    } else {
463        v
464    };
465    let v = if const { V::LANES >= 8 } {
466        key_stage::<V, O, Distance<4>, K>(v)
467    } else {
468        v
469    };
470    let v = if const { V::LANES >= 4 } {
471        key_stage::<V, O, Distance<2>, K>(v)
472    } else {
473        v
474    };
475
476    if const { V::LANES >= 2 } {
477        key_stage::<V, O, Distance<1>, K>(v)
478    } else {
479        v
480    }
481}
Last built: 2026-09-08 21:35:55 UTC