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}