Part I: TimSort (Tim Peters, 2002) — Adaptive Stable Merging
Designed by Tim Peters in 2002 for CPython (and subsequently adopted by Java Arrays.sort(Object[]), Android, and V8's Array.prototype.sort), TimSort is a natural adaptive Merge Sort built around four hardware-tuned mechanisms:
- Natural Run Detection & Reverse-Run Inversion: A linear scan identifies the longest prefix starting at index $i$ that is either non-decreasing ($A[0] \le A[1] \le \dots$) or strictly decreasing ($A[0] > A[1] > \dots$). Why strictly decreasing? Reversing a non-strictly decreasing run ($A[0] \ge A[1]$) in place would flip equal keys and violate stability!
- Minimum Run Length (
minrun$\in [32, 64]$): If a natural run is shorter thanminrun, TimSort extends it to lengthminrunusing Binary Insertion Sort. Furthermore,minrunis chosen so that $N / \text{minrun}$ is a power of $2$ (or slightly less than a power of $2$), ensuring the final top-level merges are perfectly balanced! - The Run Stack Balance Invariants: Active runs are pushed onto a stack. To ensure merges happen between runs of comparable size while bounding stack depth by $O(\log N)$, TimSort maintains two invariants on the top three run lengths $X, Y, Z$ (where $Z$ is on top of the stack): $$X > Y + Z \quad \text{and} \quad Y > Z$$ Notice this is the exact recurrence of the Fibonacci numbers! If either invariant is violated, $Y$ is merged with $\min(X, Z)$.
- Galloping Mode (
MIN_GALLOP = 7): During a two-finger merge of runs $A$ and $B$, if one run wins $7$ times in a row, TimSort switches from $1$-by-$1$ linear comparison to Galloping (Exponential + Binary Search): it probes offsets $1, 3, 7, 15, 2^k - 1$ to find the entire contiguous chunk of $A$ smaller than $B[j]$ in $O(\log m)$ comparisons and copies the whole chunk via a single vectorizedmemmove!
For 13 years, CPython and Java allocated a fixed 85-slot run stack assuming the condition X > Y + Z && Y > Z forced run lengths on the stack to grow at least as fast as Fibonacci numbers. In 2015, Stijn de Gouw et al. used formal verification (KeY) to prove that merging $Y$ with $X$ can break the invariant for the element below $X$ ($W \le X_{\text{new}} + Z$), allowing an adversarial input to overflow the fixed run stack! Modern implementations either check 4 stack elements ($W, X, Y, Z$) or replace the stack heuristic entirely with Munro & Wild's Powersort (2018) (now standard in CPython 3.11+ and PyPy), which assigns each boundary a binary tree node power in $O(1)$ bit operations (__builtin_clz(mid_A ^ mid_B)) and guarantees near-optimal merge entropy with a trivially provable $\lceil \log_2 N \rceil + 1$ stack bound.
Part II: Pattern-Defeating Quicksort (pdqsort, Orson Peters 2021)
When stability is not required (sorting primitive slices in Rust's slice::sort_unstable or Go 1.19+'s slices.Sort), allocating $O(N)$ merge space is undesirable. Orson Peters's pdqsort upgrades Musser's Introsort into an adaptive, branchless $O(1)$-auxiliary-space engine:
- Adaptive $O(N)$ Fast Paths on Pre-Sorted / Equal Data: When a partition moves zero elements (meaning no keys crossed the pivot), the subarray might already be sorted!
pdqsortruns a bounded speculative Insertion Sort (partial_insertion_sortallowing up to 8 misplaced elements)—finishing in $O(N)$ time on sorted and nearly-sorted arrays. - 3-Way Left-Partitioning on Duplicates: If the chosen pivot equals the predecessor element from the parent partition ($v == A[lo - 1]$), all keys strictly less than $v$ were already partitioned to the left. A single PartitionEqual pass packs all keys equal to $v$ and skips them!
- Branchless BlockQuicksort + Pattern Busting: Uses 64-element offset blocks to avoid branch mispredictions when types have branchless comparators, and deterministically shuffles endpoint elements when a highly unbalanced split ($< N/8$) occurs before falling back to Heapsort after $\log_2 N$ bad splits.
Explore TimSort natural run detection, Fibonacci-bounded run stack invariants (|Z| > |Y|+|X| ∧ |Y| > |X|), exponential galloping merge, and Rust pdqsort branchless offsets.
Invariant & State
| Property | TimSort / Powersort (Python, V8, Java) | pdqsort (Rust sort_unstable, Go slices.Sort) |
Classic Introsort (C++ std::sort) |
|---|---|---|---|
| Worst-Case Time | $\Theta(N \log N)$ | $\Theta(N \log N)$ (Heapsort fallback) | $\Theta(N \log N)$ (Heapsort fallback) |
| Sorted / Reverse / Equal Time | $\Theta(N)$ ($N-1$ comparisons!) | $\Theta(N)$ (speculative insertion + eq partition) | $\Theta(N \log N)$ |
| $k$ Distinct Keys Time | $O(N \log N)$ (or $O(N \log k)$ via galloping) | $\Theta(N \log k)$ (entropy-optimal) | $\Theta(N \log N)$ |
| Auxiliary Memory | Up to $N/2$ pointers ($O(N)$ worst, $O(1)$ sorted) | $O(\log N)$ stack frames ($O(1)$ heap) | $O(\log N)$ stack frames |
| Stable? | Yes (Mandatory for object/UI sorts) | No (Unstable primitive speed) | No |
min(|A|, |B|) Temp Buffer SpaceWhen merging an adjacent run $A$ of length $10{,}000$ with a run $B$ of length $400$, copying both $A$ and $B$ into a temporary buffer would waste memory bandwidth. TimSort copies only the smaller run into temporary storage: if $|A| \le |B|$, it copies $A$ to tmp and merges left-to-right into $A$'s vacated space; if $|B| < |A|$, it copies $B$ to tmp and merges right-to-left starting from the end of $B$! Combined with galloping boundary trimming (trimming elements of $A$ already smaller than $B[0]$ and elements of $B$ already larger than $A[\text{last}]$ before copying anything), TimSort often allocates only a few kilobytes of L1-resident scratch space.
Production C++20 Implementation: Galloping Search (gallop_left / gallop_right) & Speculative Insertion
#include <algorithm>
#include <concepts>
#include <cstddef>
#include <utility>
// TimSort's gallop_right: Exponential + Binary search for the first index k in a[0..n-1]
// where key < a[k] (so all elements in a[0..k-1] are <= key, preserving stability).
template <std::totally_ordered T>
std::size_t gallop_right(const T& key, const T* a, std::size_t n, std::size_t hint = 0) noexcept {
std::size_t last_ofs = 0;
std::size_t ofs = 1;
if (key < a[hint]) {
// Gallop left from hint
const std::size_t max_ofs = hint + 1;
while (ofs < max_ofs && key < a[hint - ofs]) {
last_ofs = ofs;
ofs = (ofs << 1) + 1;
}
if (ofs > max_ofs) ofs = max_ofs;
std::size_t tmp = last_ofs;
last_ofs = hint - ofs;
ofs = hint - tmp;
} else {
// Gallop right from hint: offsets 1, 3, 7, 15, ...
const std::size_t max_ofs = n - hint;
while (ofs < max_ofs && !(key < a[hint + ofs])) {
last_ofs = ofs;
ofs = (ofs << 1) + 1;
}
if (ofs > max_ofs) ofs = max_ofs;
last_ofs += hint;
ofs += hint;
}
// Standard half-open binary search inside the bracketed window (last_ofs, ofs]
std::size_t lo = last_ofs;
std::size_t hi = ofs;
while (lo < hi) {
const std::size_t mid = lo + ((hi - lo) >> 1);
if (key < a[mid]) hi = mid;
else lo = mid + 1;
}
return lo;
}
// pdqsort's Speculative Partial Insertion Sort: sorts in O(N) if <= 8 elements are out of order,
// or aborts immediately in O(1) once 8 inversions are exceeded!
template <std::totally_ordered T>
bool partial_insertion_sort(T* first, T* last) noexcept {
if (last - first <= 1) return true;
constexpr int MAX_STEPS = 8;
int steps = 0;
for (T* cur = first + 1; cur != last; ++cur) {
if (*cur < *(cur - 1)) {
if (++steps > MAX_STEPS) return false; // Abort early: array is not nearly sorted
T val = std::move(*cur);
T* j = cur;
do {
*j = std::move(*(j - 1));
--j;
} while (j != first && val < *(j - 1));
*j = std::move(val);
}
}
return true;
}
min_gallop AdjustmentOn uniformly random data, galloping performs $\approx 2\log_2 k$ comparisons to advance just $k = 1$ or $2$ elements—more comparisons than a simple linear merge! To prevent galloping from slowing down random inputs, TimSort dynamically adjusts its threshold min_gallop: every time galloping succeeds in copying at least MIN_GALLOP = 7 elements, min_gallop is decremented by $1$ (making re-entry into galloping easier); every time galloping exits early because both runs failed to yield a block of $\ge 7$ elements, min_gallop is incremented by $1$, smoothly disabling galloping on noisy data.