1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
use mem;
/// Sorts a slice in place using stooge sort.
///
/// Stooge sort compares the first and the last element and swaps them if they are out of
/// order. Then, if the slice has at least three elements, it stooge-sorts its first two thirds
/// (rounded up), its last two thirds, and its first two thirds again. The first call moves
/// every one of the largest third of the elements that starts in the first two thirds into the
/// middle third, so after the second call all of them sit at the end, in order; the third call
/// sorts the rest. The algorithm is Problem 7-3 of Cormen, Leiserson, Rivest and Stein's
/// *Introduction to Algorithms*, where Professors Howard, Fine and Howard, the Three Stooges,
/// propose it as an "elegant" sorting algorithm, and the reader shows that it takes
/// `Θ(n^(log 3 / log 1.5)) ≈ Θ(n^2.71)` time: worse than bubble sort. Which elements are
/// compared depends only on the length of the slice, never on the answers, so stooge sort is a
/// sorting network: one fixed sequence of compare-exchanges for each length.
///
/// The realisation keeps that sequence exactly — every comparison the textbook recursion makes,
/// in the same order, with the same swaps — and executes it as cheaply as it can:
///
/// * **Unrolled bottom levels.** All three parts of a part of `m` elements have
/// `m − ⌊m/3⌋` elements, so all parts at one depth of the recursion have the same length,
/// and the parts four levels above the bottom have 7, 8 or 9 elements throughout one sort.
/// They run as straight-line code: the 121 compare-exchanges of the recursion below them, at
/// constant indices, with no calls and no index checks. The textbook makes one call per
/// compare-exchange, and the calls are its cost: recursing down to two elements without
/// index checks is only 1.05–1.15 times faster than it, while unrolling four levels is 5–8
/// times faster for `u64`, 1.4–1.65 times for strings and 1.4–2.3 times for 64-byte elements
/// than recursing down to two elements. Unrolling two or three levels (parts of at most 4 or
/// 6 elements) was 1.0–1.65 times slower, and five levels (at most 13) at most 1.05 times
/// faster, for four times the code.
/// * **A branch, not a branchless select.** Only about 1% of the compare-exchanges swap
/// anything, so a branch is predicted almost always and the compare-exchanges do not wait
/// for one another. A branchless select chains each to the one before: measured 2 times
/// slower than the textbook for `u64`, 4 times for strings and 1.8 times for 64-byte
/// elements.
/// * **The swap is marked cold**, so the compare-exchanges that do not swap are laid out as
/// one straight line: 1.5–1.85 times faster for 64-byte elements, 1.05–1.25 times for
/// strings and 1.1–1.25 times for `u64` from 100 elements on.
///
/// Together, against the textbook: 9–11 times faster for `u64` from 64 elements on (3–10 times
/// below), 1.7–2.2 times for strings, and 4.3–5 times for 64-byte elements from 32 on (2.5–4.5
/// times below; 1.4–2 times with few distinct keys, where every comparison reads all 64
/// bytes). Against a first version that recursed down to two elements
/// with a branchless compare-exchange: 16–22 times faster for `u64` from 64 elements on.
///
/// The code is safe Rust: indices into the unrolled parts are constants, and the parts above
/// them are cut with checked slicing, once per part.
///
/// # Properties
///
/// * **Unstable**: the compare-exchange of the first and the last element jumps over
/// everything between them. `[1a, 1b, 0]` becomes `[0, 1b, 1a]`.
/// * **In place**: no allocation. The recursion nests two calls per level (the compiler turns
/// the third into a loop), `D(n) − 4` levels for `n ≥ 10`, where `D(n) ≈ log₁.₅ n` is the depth
/// defined below: 12 levels for 1000 elements.
/// * **Not adaptive**: every input of `n` elements costs the same comparisons. Already-sorted
/// input is never moved: only a strictly smaller later element is swapped forward.
///
/// Let `D(n)` be the number of times `m ↦ m − ⌊m/3⌋` must be applied to `n` to reach 2 (the
/// depth of the recursion). Every part makes one comparison and has three parts below it, down
/// to depth `D(n)`, so stooge sort of `n ≥ 2` elements makes exactly
/// `C(n) = 1 + 3 + … + 3^D(n) = (3^(D(n) + 1) − 1)/2` comparisons, on every input
/// (equivalently `C(2) = 1` and `C(n) = 1 + 3·C(n − ⌊n/3⌋)`). The number of swaps depends on
/// the input and is at most `C(n)`; on random input it is about 1% of it.
///
/// | `n` | `D(n)` | Comparisons, every input | Bubble sort, worst |
/// |---------|--------|--------------------------|--------------------|
/// | 2 | 0 | 1 | 1 |
/// | 4 | 2 | 13 | 6 |
/// | 8 | 4 | 121 | 28 |
/// | 16 | 6 | 1,093 | 120 |
/// | 100 | 11 | 265,720 | 4,950 |
/// | 1000 | 16 | 64,570,081 | 499,500 |
///
/// `D(n)` grows in steps, so 100 and 128 elements cost the same; on average `C(n)` grows as
/// `n^2.71`.
///
/// Stooge sort is mainly of educational value, as an example of a correct algorithm with a
/// surprising running time; prefer [`slice::sort`] for real workloads. [`slice::sort_unstable`]
/// is 1.2–4 times faster on 8 elements, 50–130 times on 100 random elements, and 900–2400
/// times on 1000 (for `u64`, strings and 64-byte elements).
///
/// # Panics
///
/// Does not panic by itself. If the [`Ord`] implementation of `T` panics, the panic is
/// propagated and `arr` is left in an unspecified order, but it still holds every original
/// element exactly once: every compare-exchange compares before it swaps. An [`Ord`] that is
/// not a total order leaves `arr` in an unspecified order with every element exactly once,
/// after exactly `C(n)` comparisons, since which elements are compared never depends on the
/// answers.
///
/// # Examples
///
/// ```
/// use a::sort::stooge;
///
/// let mut v = [5, 1, 4, 2, 8, 0];
/// stooge(&mut v);
/// assert_eq!(v, [0, 1, 2, 4, 5, 8]);
///
/// let mut words = vec!["pear", "apple", "fig"];
/// stooge(&mut words);
/// assert_eq!(words, ["apple", "fig", "pear"]);
///
/// // Not stable: sorting by the first field carries 'a' past 'b'.
/// #[derive(Debug, PartialEq, Eq)]
/// struct Card(u8, char);
/// impl Ord for Card {
/// fn cmp(&self, other: &Self) -> std::cmp::Ordering {
/// self.0.cmp(&other.0)
/// }
/// }
/// impl PartialOrd for Card {
/// fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
/// Some(self.cmp(other))
/// }
/// }
/// let mut cards = [Card(1, 'a'), Card(1, 'b'), Card(0, 'c')];
/// stooge(&mut cards);
/// assert_eq!(cards, [Card(0, 'c'), Card(1, 'b'), Card(1, 'a')]);
/// ```
/// The longest part that runs as straight-line code: four levels of recursion, 121
/// compare-exchanges on 7 to 9 elements.
///
/// Measured against unrolling fewer or more levels of the same code, relative to this choice
/// (`u64`, strings, 64-byte elements; 16 to 500 elements): down to parts of 2 elements, that
/// is plain recursion, 4.9–8.2, 1.5–1.65 and 1.4–2.3 times slower; of at most 4 elements
/// 1.4–1.65, 1.03–1.09 and 1.03–1.23 times slower; of at most 6 elements 1.0–1.17, 1.01–1.06
/// and 1.0–1.09 times slower. At most 13 elements (364 compare-exchanges, five levels) is
/// within 0.95–1.12 of it, but compiles to four times the code (1917 compare-exchanges
/// instead of 461) for every element type.
const UNROLLED_MAX: usize = 9;
/// Returns the length of the parts of a slice of `n > UNROLLED_MAX` elements at the first
/// depth of the recursion where they are at most [`UNROLLED_MAX`] long: 7, 8 or 9.
///
/// Every part of `m` elements recurses into parts of `m − ⌊m/3⌋`, so all parts at one depth
/// have the same length; `m − ⌊m/3⌋` grows with `m` and is 7 for `m = 10`, so a part longer
/// than 9 never has parts shorter than 7.
/// Stooge-sorts `v`, whose parts at some depth of the recursion (or `v` itself) are `M`
/// elements long; those parts run as straight-line code.
///
/// A slice of any other length — which [`stooge`] never passes — is still left a permutation
/// of itself: all indexing is checked or into arrays of length `M`.
/// Stooge-sorts `w` with the recursion fully unrolled: one fixed sequence of compare-exchanges
/// at constant indices, so no index is checked at run time and, for small `Copy` types, the
/// elements stay in registers throughout.
/// Swaps `w[i]` and `w[j]` (`i < j`) if `w[j]` is smaller.
///
/// A plain branch, not a branchless select: only about 1% of the compare-exchanges of stooge
/// sort swap anything (4.5% at 16 random elements, 1% from 64 on, 0.2% with few distinct
/// keys), so the branch is predicted almost always, and the compare-exchanges do not wait for
/// one another. A branchless select chains every compare-exchange to the one before: measured
/// 2 times slower than even the textbook recursion for `u64`, 4 times for strings and 1.8
/// times for 64-byte elements.
///
/// The swap is marked as the unlikely side of the branch (see [`unlikely`]). [`mem::swap`] of
/// two disjoint elements, rather than [`slice::swap`], keeps the stack frame small: the latter
/// reserved a temporary element for each of the 121 inlined swaps, 7.7 KiB per level of the
/// recursion for 64-byte elements.
/// Does nothing; a call to it marks the code around it as rarely run.
///
/// Calls to a `#[cold]` function tell LLVM that the branch leading to them is unlikely, so it
/// lays out the compare-exchanges that do not swap as one straight line and moves the swaps out
/// of it. The call itself stays (callers instantiate [`stooge`] in their own crate, where this
/// body is not visible, unless link-time optimisation inlines it) and costs a few cycles per
/// swap. Measured against the same code without it: 1.5–1.85 times faster for 64-byte elements
/// (1.0–1.05 times with few distinct keys, whose comparisons dominate), 1.05–1.25 times for
/// strings, and 1.1–1.25 times for `u64` from 100 elements on, where LLVM now reloads elements
/// instead of carrying them in registers; on 8 to 32 `u64` it is anywhere from 1.35 times
/// faster to 1.3 times slower, as more of the few compare-exchanges swap there. Calling a cold
/// function that does the swap itself did as well for strings and 64-byte elements, but was up
/// to 1.5 times slower on 8 to 32 `u64`. (`core::hint::cold_path` would say this directly, but
/// is not stable in Rust 1.88.)
/// Defines `$name(w, lo)`: stooge sort of the `$len` elements `w[lo..lo + $len]`, unrolled,
/// with `$sub` the unrolled stooge sort of its parts of `$len − ⌊$len/3⌋` elements.
/// Stooge-sorts `w[lo..lo + 2]`: a single compare-exchange.
unrolled_stooge!;
unrolled_stooge!;
unrolled_stooge!;
unrolled_stooge!;
unrolled_stooge!;
unrolled_stooge!;
unrolled_stooge!;