Home

Awesome

Intro

This document describes a hybrid quicksort / mergesort named crumsort. The sort is in-place, unstable, adaptive, branchless, and has exceptional performance.

Analyzer

Crumsort starts out with an analyzer that sorts fully in-order or reverse-order arrays using n comparisons. It also obtains a measure of presortedness for 4 segments of the array and switches to quadsort if a segment is more than 50% ordered.

Partitioning

Partitioning is performed in a top-down manner similar to quicksort. Crumsort obtains the pseudomedian of 9 for partitions smaller than 2048 elements, and the median of 16 for paritions smaller than 65536. For larger partitions crumsort obtains the median of 128, 256, or 512 as an approximation of the cubic root of the partition size. While the square root is optimal in theory, the law of diminishing returns appears to apply to increasing the number of pivot candidates. Hardware limitations need to be factored in as well.

For large partitions crumsort will swap 128-512 random elements to the start of the array, sort them with quadsort, and take the center right element. Using pseudomedian instead of median selection on large arrays is slower, likely due to cache pollution.

The median element obtained will be referred to as the pivot. Partitions that grow smaller than 24 elements are sorted with quadsort.

Fulcrum Partition

After obtaining a pivot the array is parsed from start to end using the fulcrum partitioning scheme. The scheme is similar to the original quicksort scheme known as the Hoare partition with some notable differences. The differences are perhaps best explained with two code examples.

int hoare_partition(int array[], int head, int tail)
{
        int pivot = head++;
        int swap;

        while (1)
        {
                while (array[head] <= array[pivot] && head < tail)
                {
                        head++;
                }

                while (array[tail] > array[pivot])
                {
                        tail--;
                }

                if (head >= tail)
                {
                        swap = array[pivot]; array[pivot] = array[tail]; array[tail] = swap;

                        return tail;
                }
                swap = array[head]; array[head] = array[tail]; array[tail] = swap;
        }
}
int fulcrum_partition(int array[], int head, int tail)
{
        int pivot = array[head];

        while (1)
        {
                if (array[tail] > pivot)
                {
                        tail--;
                        continue;
                }

                if (head >= tail)
                {
                        array[head] = pivot;
                        return head;
                }
                array[head++] = array[tail];

                while (1)
                {
                        if (head >= tail)
                        {
                                array[head] = pivot;
                                return head;
                        }

                        if (array[head] <= pivot)
                        {
                                head++;
                                continue;
                        }
                        array[tail--] = array[head];
                        break;
                }
        }
}

Instead of using multiple swaps the fulcrum partition creates a 1 element swap space, with the pivot holding the original data. Doing so turns the 3 assignments from the swap into 2 assignments. Overall the fulcrum partition has a 10-20% performance improvement.

The swap space of the fulcrum partition can easily be increased from 1 to 32 elements to allow it to perform 16 boundless comparisons at a time, which in turn also allows it to perform these comparisons in a branchless manner with little additional overhead.

Worst case handling

To avoid run-away recursion crumsort switches to quadsort for both partitions if one partition is less than 1/16th the size of the other partition. On a distribution of random unique values the observed chance of a false positive is 1 in 1,336 for the pseudomedian of 9 and approximately 1 in 500,000 for the median of 16.

Combined with the analyzer crumsort starts out with this makes the existence of killer patterns unlikely, other than a 33-50% performance slowdown by prematurely triggering the use of quadsort.

Branchless optimizations

Crumsort uses a branchless comparison optimization. The ability of quicksort to partition branchless was first described in "BlockQuicksort: How Branch Mispredictions don't affect Quicksort" by Stefan Edelkamp and Armin Weiss. Crumsort uses the fulcrum partitioning scheme where as BlockQuicksort uses a scheme resembling Hoare partitioning.

Median selection uses a branchless comparison technique that selects the pseudomedian of 9 using 12 comparisons, and the pseudomedian of 25 using 42 comparisons.

These optimizations do not work as well when the comparisons themselves are branched and the largest performance increase is on 32 and 64 bit integers.

Generic data optimizations

Crumsort uses a method that mimicks dual-pivot quicksort to improve generic data handling. If after a partition all elements were smaller or equal to the pivot, a reverse partition is performed, filtering out all elements equal to the pivot, next it carries on as usual. This typically only occurs when sorting tables with many identical values, like gender, age, etc. Crumsort has a small bias in its pivot selection to increase the odds of this happening. In addition, generic data performance is improved slightly by checking if the same pivot is chosen twice in a row, in which case it performs a reverse partition as well. Pivot retention was first introduced by pdqsort.

Small array optimizations

Most modern quicksorts use insertion sort for partitions smaller than 24 elements. Crumsort uses quadsort which has a dedicated small array sorting routine that outperforms insertion sort.

Data Types

The C implementation of crumsort supports long doubles and 8, 16, 32, and 64 bit data types. By using pointers it's possible to sort any other data type, like strings.

Interface

Crumsort uses the same interface as qsort, which is described in man qsort.

Crumsort comes with the crumsort_prim(void *array, size_t nmemb, size_t size) function to perform primitive comparisons on arrays of 32 and 64 bit integers. Nmemb is the number of elements. Size should be either sizeof(int) or sizeof(long long) for signed integers, and sizeof(int) + 1 or sizeof(long long) + 1 for unsigned integers. Support for additional primitive as well as custom types can be added to fluxsort.h and quadsort.h.

Porting

People wanting to port crumsort might want to have a look at fluxsort, which is a little bit simpler because it's stable and out of place. There's also piposort, a simplified implementation of quadsort.

Memory

Crumsort uses 512 elements of stack memory, which is shared with quadsort. Recursion requires log n stack memory.

Crumsort can be configured to use sqrt(n) memory, with a minimum memory requirement of 32 elements.

Performance

Crumsort will begin to outperform fluxsort on random data right around 1,000,000 elements. Since it runs on 512 elements of auxiliary memory the sorting of ordered data will be slower than fluxsort for larger arrays.

Crumsort being unstable will scramble pre-existing patterns, making it less adaptive than fluxsort, which will switch to quadsort when it detects the emergence of ordered data during the partitioning phase.

Because of the partitioning scheme crumsort is slower than pdqsort when sorting arrays of long doubles. Fixing this is on my todo list and I've devised a scheme to do so. The main focus of crumsort is the sorting of tables however, and crumsort will outperform pdqsort when long doubles are embedded within a table. In this case crumsort only has to move a 64 bit pointer, instead of a 128 bit float.

To take full advantage of branchless operations the cmp macro needs to be uncommented in bench.c, which will increase the performance by 100% on primitive types. The crumsort_prim() function can be used to access primitive comparisons directly. In the case of 64 bit integers crumsort will outperform all radix sorts I've tested so far. Radix sorts still hold an advantage for 32 bit integers on arrays under 1 million elements.

Big O

                 ┌───────────────────────┐┌───────────────────────┐
                 │comparisons            ││swap memory            │
┌───────────────┐├───────┬───────┬───────┤├───────┬───────┬───────┤┌──────┐┌─────────┐┌─────────┐
│name           ││min    │avg    │max    ││min    │avg    │max    ││stable││partition││adaptive │
├───────────────┤├───────┼───────┼───────┤├───────┼───────┼───────┤├──────┤├─────────┤├─────────┤
│fluxsort       ││n      │n log n│n log n││1      │n      │n      ││yes   ││yes      ││yes      │
├───────────────┤├───────┼───────┼───────┤├───────┼───────┼───────┤├──────┤├─────────┤├─────────┤
│quadsort       ││n      │n log n│n log n││1      │n      │n      ││yes   ││no       ││yes      │
├───────────────┤├───────┼───────┼───────┤├───────┼───────┼───────┤├──────┤├─────────┤├─────────┤
│quicksort      ││n log n│n log n│n²     ││1      │1      │1      ││no    ││yes      ││no       │
├───────────────┤├───────┼───────┼───────┤├───────┼───────┼───────┤├──────┤├─────────┤├─────────┤
│crumsort       ││n      │n log n│n log n││1      │1      │1      ││no    ││yes      ││yes      │
└───────────────┘└───────┴───────┴───────┘└───────┴───────┴───────┘└──────┘└─────────┘└─────────┘

Variants

Visualization

In the visualization below two tests are performed on 512 elements.

  1. Random order
  2. Random % 10

The upper half shows the swap memory (32 elements) and the bottom half shows the main memory. Colors are used to differentiate various operations.

crumsort benchmark

Benchmarks

The following benchmark was on WSL gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) using the wolfsort benchmark. The source code was compiled using g++ -O3 -w -fpermissive bench.c. Each test was ran 100 times on 100,000 elements. A table with the best and average time in seconds can be uncollapsed below the bar graph. Comparisons for fluxsort, crumsort and pdqsort are inlined.

fluxsort vs crumsort vs pdqsort

<details><summary>data table</summary>
NameItemsTypeBestAverageComparesSamplesDistribution
pdqsort1000001280.0059080.0059750100random order
crumsort1000001280.0082620.0083160100random order
fluxsort1000001280.0085670.0086760100random order
NameItemsTypeBestAverageComparesSamplesDistribution
pdqsort100000640.0026450.0026680100random order
crumsort100000640.0018960.0019270100random order
fluxsort100000640.0019420.0019650100random order
NameItemsTypeBestAverageLoopsSamplesDistribution
pdqsort100000320.0026820.0027050100random order
crumsort100000320.0017970.0018120100random order
fluxsort100000320.0018340.0018580100random order
pdqsort100000320.0008010.0008070100random % 100
crumsort100000320.0005600.0005750100random % 100
fluxsort100000320.0006570.0006700100random % 100
pdqsort100000320.0000980.0000990100ascending order
crumsort100000320.0000430.0000460100ascending order
fluxsort100000320.0000440.0000440100ascending order
pdqsort100000320.0034600.0034830100ascending saw
crumsort100000320.0006280.0006380100ascending saw
fluxsort100000320.0003280.0003360100ascending saw
pdqsort100000320.0028280.0028500100pipe organ
crumsort100000320.0003590.0003630100pipe organ
fluxsort100000320.0002150.0002190100pipe organ
pdqsort100000320.0002010.0002030100descending order
crumsort100000320.0000550.0000550100descending order
fluxsort100000320.0000550.0000560100descending order
pdqsort100000320.0032290.0032600100descending saw
crumsort100000320.0006370.0006450100descending saw
fluxsort100000320.0003280.0003320100descending saw
pdqsort100000320.0025580.0025790100random tail
crumsort100000320.0008790.0008950100random tail
fluxsort100000320.0006260.0006310100random tail
pdqsort100000320.0026600.0026770100random half
crumsort100000320.0012000.0012070100random half
fluxsort100000320.0010690.0010840100random half
pdqsort100000320.0023100.0023280100ascending tiles
crumsort100000320.0015200.0015340100ascending tiles
fluxsort100000320.0002940.0002980100ascending tiles
pdqsort100000320.0026590.0026810100bit reversal
crumsort100000320.0017870.0018000100bit reversal
fluxsort100000320.0016960.0017210100bit reversal
</details>

fluxsort vs crumsort vs pdqsort

<details><summary>data table</summary>
NameItemsTypeBestAverageComparesSamplesDistribution
pdqsort10320.0871400.0874360.010random 10
crumsort10320.0499210.0501320.010random 10
fluxsort10320.0484990.0487240.010random 10
pdqsort100320.1695830.1699400.010random 100
crumsort100320.1134430.1138820.010random 100
fluxsort100320.1134260.1139550.010random 100
pdqsort1000320.2072000.2078580.010random 1000
crumsort1000320.1359120.1362510.010random 1000
fluxsort1000320.1370190.1385860.010random 1000
pdqsort10000320.2382970.2390060.010random 10000
crumsort10000320.1582490.1584760.010random 10000
fluxsort10000320.1584450.1586940.010random 10000
pdqsort100000320.2704470.2708550.010random 100000
crumsort100000320.1817700.1831230.010random 100000
fluxsort100000320.1859070.1868290.010random 100000
pdqsort1000000320.3035250.3054670.010random 1000000
crumsort1000000320.2069790.2081530.010random 1000000
fluxsort1000000320.2150980.2162940.010random 1000000
pdqsort10000000320.3387670.342580010random 10000000
crumsort10000000320.2342680.234664010random 10000000
fluxsort10000000320.2649880.267283010random 10000000
</details>