brainsort

version 0.5.0stable sortheader-only C++20CI status

A stable sorting algorithm for keys that map to an ordered integer: numbers, strings, dates, ids. This page is the C++ library: measured against the stable sorts that ship in today's C++ standard libraries and in the common libraries, on the same inputs, the same compiler flags and the same machines. The Rust crate has its own page with the same sections, built from its own measurements.

In your own code

Use it

Copy the single header into your project, or add include/ to your include path and include brainsort/brainsort.hpp. Header-only C++20, MIT licensed. GCC, Clang, MSVC and Apple Clang; x86-64 with AVX2 and BMI2 chosen at run time, ARM64 and 32-bit x86 with the same code minus the vector paths.

#include "brainsort.hpp"

std::vector<int> v = ...;
brainsort::sort(v);                                              // stable, by value
brainsort::sort(v.begin(), v.end());                             // any random-access range
brainsort::sort(rows, [](const Row& r) { return r.id; });        // by a key
brainsort::sort(rows, [](const Row& r) { return std::pair(r.group, brainsort::desc(r.score)); });
brainsort::sort(rows, [](const Row& a, const Row& b) { return a.name < b.name; });  // a comparator: merge sort
Keys

Any integer, bool, character type, float, double, enum, pointer, std::string, std::string_view, C string, std::chrono duration or time point, a std::pair, std::tuple or std::array of those, or brainsort::desc(key) for a reversed order. Your own key type takes a brainsort::key_traits specialisation.

Elements

Anything move-assignable, in any random-access range: std::vector, std::deque, std::array, std::span, C arrays, pointer pairs. The keys are sorted first, then the elements are permuted once. Every overload 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 gets a comparison sort, the library's own stable merge sort, without the radix wins. A range of 232 elements or more goes to std::stable_sort.

The full contract, every overload and how the elements are handled: README, The library; step by step with examples: the C++ 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 std::stable_sort, cpp-TimSort and Boost.Sort's spinsort and flat_stable_sort; std::sort, pdqsort and the radix libraries 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:

brainsortupstream code, 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 bytes an algorithm moves, how many comparisons it makes, how much memory it asks for. 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

Measured time per machine measured

brainsortupstream code, as shipped
machine

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

All machines side by side

What a user gets

The library 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.
  • Measured numbers come from repeated timed runs of the plain build: the median of the repetitions, and the best median of two rounds over the whole matrix, because interference can only make a run slower. Below 100,000 elements the timed region sorts enough independent copies to reach 100,000 elements and reports the time per sort, so the clock's own cost stays small. Above, the repetitions shrink in proportion, never below three.
  • All algorithms are compiled with the same flags (-O2, no -march=native). brainsort chooses its AVX2 and BMI2 paths at run time.
  • The library on plain vectors is a separate benchmark: brainsort::sort on a std::vector against std::stable_sort, cpp-TimSort and the two Boost.Sort stable sorts on the same generated inputs, the median of a few runs, every result checked, the same flags as above.
  • 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

sh scripts/counts.sh        # the deterministic numbers (results/counts.csv, any machine)
sh scripts/bench.sh         # the timing of this machine, then both pages of the website
python3 scripts/website.py     # the website alone, from results/

Windows: scripts\counts.ps1, scripts\bench.ps1. 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 upstream code run as shipped, counted through an element wrapper and the allocator; nothing is a port written here. The pool is the C++ sorts that can do what brainsort does: stable, every key type, an arbitrary comparator. std::sort, pdqsort and the radix libraries 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. Rust 1.81+ ships driftsort and CPython 3.11+ merges with powersort; the Rust page counts driftsort as shipped, on what a Rust program can observe.

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.