brainsort

version 0.5.0stable sortRust crateCI status

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.

In your own code

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
Keys

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.

Elements

Anything, in a slice or a Vec. The keys are sorted first, then the elements are permuted once. Every function is stable.

Three things to know

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.

Overview

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.

Reading guide

How to read this page

Six things to know, then every number below makes sense.

1A cell

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.

2Lower is better

On every number here: fewer bytes moved, fewer comparisons, less memory, less time.

3Two kinds of numbers

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.

4Only like for like

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.

5Sizes

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.

6Colours

Blue: brainsort ahead. Red: behind. Grey: a tie within 5%. Darker is a larger margin. In charts:

brainsortthe Rust sorts, as shipped
size
key type
inputs
Same on every machine

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

brainsort behind
brainsort aheadgrey: a tie within 5%. Each cell is the best other sort divided by brainsort.

Across sizes

input

One cell in detail

key typeinput
Full tables for this metric
Per machine: what a user gets

The crate on plain vectors measured

machine
elements

brainsort behind
brainsort aheadgrey: a tie within 5%. Each cell is the best other sort divided by brainsort.

All machines side by side

C++ and Rust on the same machine

Behind the numbers

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::sort on a Vec against 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

Raw data

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.