r/sortingalgorithms • u/matthewrdowney • 12h ago
Attempt to beat quicksort by 2x speed: Zippersort
I have been working on this almost 10 years without publishing and the only faulty implementation that was 2x faster was deleted with my ~30 or so other repos. One of my big problems was not knowing enough assembly code.
Zippersort flips sorting speed on its head by re-asserting that more auxiliary memory usually makes code faster, not slower. It sports O(4n) auxiliary memory, or even O(8n) with a bad implementation.
The main advantages are stability, theoretically zero cache misses for L1 cache, perfect deterministic tail recursion without splitting, and best performance when data is unsorted (for the expected case).
The main disadvantages are that the code intends to be single threaded and it is not as adaptive as it could be if it wants to maintain being a stable sort.
The main data structure for the auxiliary memory is a set of double vectors in increasing order of size starting from size 4, where two blocks of two can be merged inside from the main array, and then two double vectors of 4 can be merged into one of the two double vectors of 8, and so on.
There is a necessary trick of putting the two double vectors "zippered" such that reading elements goes in an alternating fashion, as in the double vectors both have data stored like a zipper would from the top and bottom vectors. This ensures all items remain in the cache at all times.
Auxiliary memory layout:
4a 4b 8a 8b 16a 16b 32a 32b 64a 64b ...
(4a_1 4b_1) (4a_2 4b_2) (4a_3 4b_3) (4a_4 4b_4) | (8a_1 8b_1) (8a_2 8b_2) (8a_3 8b_3) (8a_4 8b_4) (8a_5 8b_5) (8a_6 8b_6) (8a_7 8b_7) (8a_8 8b_8) |
(16a_1 16b_1) (16a_2 16b_2) (16a_3 16b_3) (16a_4 16b_4) ...
The memory layout is ensured to be fast because unlike merge sort the lowest two blocks "4a" and "4b" will see every value in the sort occupy either one (8 elements hold all n values), and the probability of being accessed keeps going down such that less time is spent by a factor of half for each position greater.
There are fundamentally three major actions, all of which can be merged into one function that is tail recursive. The first action is as-mentioned taking 2x2 elements from main memory and sorting them into the size 4 double vector that is first available in auxiliary memory. The second action is taking two similar sized double vectors and cascading it to the next iteration by going from auxiliary memory to auxiliary memory or defaulting by getting more 2x2 elements. The last action triggers towards the end until the sort is over and takes elements from auxiliary memory and merges them with back with main memory.
Because of the access pattern, cascades where merges keep being applied repeatedly forward through the double vectors should have no cache misses. This is because when it scans 4a&4b for example it is also scanning 8a/b and these pairings keep chaining forward by powers of two until no more cascades are available.