A stable sorting algorithm for keys that map to an ordered integer: numbers, strings, dates, ids. This page is the Rust crate: the same algorithm as the C++ library, held to the same deterministic counts by its tests, and timed on its own against the stable sorts the Rust ecosystem ships, on the same inputs and the same machines. The C++ library has its own page with the same sections.
Use it
Add the crate brainsort to your project. It is a port of the C++ library function by function, held to the C++ golden numbers by its test suite: every element read and write, every comparison and table access, the peak memory and the exact access sequence must match, cell for cell. No dependencies, no_std + alloc, MIT licensed; AVX2 and BMI2 chosen at run time on x86-64, the same code minus the vector paths elsewhere.
[dependencies]
brainsort = "0.5.0"
let mut v = vec![3, 1, 2];
brainsort::sort(&mut v); // stable, by value
brainsort::sort_by_key(&mut rows, |r| r.id); // by a key
brainsort::sort_by_key_ref(&mut rows, |r| r.name.as_str()); // by a borrowed key
brainsort::sort_by_key(&mut rows, |r| (r.id, brainsort::Desc(r.score)));
brainsort::sort_by(&mut rows, |a, b| a.name.cmp(&b.name)); // a comparator
Every integer, bool, char, f32, f64, Duration, str, String, byte strings, tuples and arrays of those, or Desc(key) for a reversed order. Your own key type implements Key.
Anything, in a slice or a Vec. The keys are sorted first, then the elements are permuted once. Every function is stable.
It uses memory: about 1.5 small records per element while sorting, plus one element per element for the final permutation of plain structs, and it keeps up to 32 MiB of freed blocks for the next call (brainsort::release_memory() frees them). An arbitrary comparator (sort_by) gets a comparison sort, the crate's own stable merge sort, without the radix wins. A slice of 232 elements or more goes to the standard library's stable sort.
The API docs are on docs.rs; step by step with examples: the Rust usage guide.
At a glance
Why these sorts. An opponent has to offer what brainsort offers, so that a win or a loss means something: it is stable, it sorts every key type on this page and it takes an arbitrary comparator. And it is the upstream code as it ships, never a port written here. That is slice::sort, its cached-key form and glidesort; slice::sort_unstable and the radix crates are unstable or sort scalar keys only, an easier job, and are not opponents.
How to read this page
Six things to know, then every number below makes sense.
One key type on one input pattern at one size. In every cell brainsort is compared with the best of the other sorts shown, whichever that is. 38 of 45 means brainsort is best or tied in 38 cells.
On every number here: fewer bytes moved, fewer comparisons, less memory, less time.
deterministic numbers describe what an algorithm does: bytes moved, comparisons made, memory asked for. Counted, the same on every machine, held by the test suite. measured numbers are clocks and hardware counters: they depend on the machine and vary between runs, so they are shown per machine.
Every sort on this page is stable (it keeps equal keys in their original order, which sorting records by one field needs), sorts every key type shown and takes an arbitrary comparator, and every one is upstream code run as shipped. A sort that does less (an unstable sort, a radix sort for scalar keys only) has an easier job and is not an opponent; a port written here could be slower than the original and is not one either.
Every number exists at several input sizes. The size control changes the whole page; the across sizes charts show a metric per element as the input grows.
Blue: brainsort ahead. Red: behind. Grey: a tie within 5%. Darker is a larger margin. In charts:
What each algorithm does deterministic
These numbers are the same on every computer. They are counted, not timed: how many comparisons a sort makes, how often their outcome flips, how often the cached-key sort asks for a key, how much memory it asks for. The other sorts are the stable sorts a Rust program can call, as shipped: the standard library's driftsort, its sort by a cached key and the glidesort crate, counted through what a program can observe of them (the comparator they are given, the key function they call, the allocator). What cannot be observed of a sort one did not write, the bytes it moves, is counted for brainsort alone in the golden file and compared with the C++ pool on the C++ page; the crate's own bytes moved are exactly those, held by its test suite. Choose a metric:
Where brainsort wins and loses
Across sizes
One cell in detail
Full tables for this metric
Per machine: what a user getsThe crate on plain vectors measured
All machines side by side
C++ and Rust on the same machine
Method
How the numbers are made
- Every (size, key type, input, algorithm) cell runs in a fresh process, pinned to one CPU where the OS allows it, so heap state and memory figures do not depend on what ran before.
- The input comes from a fixed seed and is the same for every algorithm. Every element carries its original position, so the harness can check the result: keys in order, no element lost or duplicated, and for a stable sort the exact order of equal keys.
- Deterministic numbers come from one run of an instrumented build that counts every element read and write, every comparison, every table access and every allocation, and feeds the access sequence to a fixed cache model. The upstream code is counted through an element wrapper with the same definitions, so a swap is two reads and two writes for everyone, and through the global allocator (and, for Boost.Sort, its malloc calls) for scratch memory.
- The crate on plain vectors is the timed measurement of the Rust crate:
brainsort::sorton aVecagainst the standard library's stable sorts and the glidesort crate as published on crates.io, on the same generated inputs as the C++ benchmark, the median of a few runs, every result checked. Everything is built by cargo in release mode with no target-cpu flags; brainsort chooses its AVX2 and BMI2 paths at run time. The algorithm-level harness with instrumented ports and hardware counters is C++ code and is on the C++ page; its deterministic counts above hold for the crate. - Where both languages were measured on one machine, the same machine id pairs them, so a C++ and a Rust number side by side always come from the same computer.
- Every timing file carries a fingerprint of the code it measured. This page is built from the current sources; a file that measured other code is left out, so no time on this page describes older code than the numbers next to it.
Reproduce it
cd rust
cargo test --release # the crate against the golden counts, among the tests
cargo run --release -p brainsort-bench -- --golden # the counted view of every cell, checked against results/counts.csv
cargo run --release -p brainsort-bench -- --rust-counts # the Rust sorts as shipped: results/rust-counts.csv, the deterministic numbers of this page
cargo run --release -p brainsort-bench -- --rust-golden # that file recomputed and checked
cargo run --release -p brainsort-bench -- --bench --out ../results/<id>.rust.api.md # the timing of this machine
python3 ../scripts/website.py # both pages of the website, from results/
scripts/bench.sh and scripts\bench.ps1 run the Rust benchmark after the C++ one when cargo is installed. Details in setup.md.
What each metric means
The algorithms
"Best opponent" on this page means the best of the other sorts in a cell. Every sort here is the published code, run unchanged: the deterministic numbers count what it asks of the program (comparisons, key reads, memory), the timed numbers what it costs. The pool is the Rust sorts that can do what brainsort does: stable, every key type, an arbitrary comparator. slice::sort_unstable and the radix crates (radsort, voracious_radix_sort, rdst) are not in it, because an unstable sort or a scalar-only radix sort has an easier job, so a comparison with them would not be like for like; the Rust ecosystem has two such sorts, the standard library's and glidesort, and the cached-key form of the standard one is the route a program takes for a sort by key. Element moves cannot be observed from outside in Rust, so bytes moved, reads, writes and the cache model are not counted for these sorts; the C++ page has them for the C++ pool.
The key types
The inputs
All numbers
Every value behind this page, for the selected size and key types: the deterministic columns once, the measured columns per machine. Open a cell to render it.