Last active
July 3, 2023 01:36
-
-
Save hez2010/6b52929ee1755788c34818972c46aefb to your computer and use it in GitHub Desktop.
PdqSort C# Port
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// PdqSort: https://github.com/orlp/pdqsort | |
using System.Numerics; | |
using System.Runtime.CompilerServices; | |
using System.Runtime.InteropServices; | |
using BenchmarkDotNet.Attributes; | |
using BenchmarkDotNet.Configs; | |
using BenchmarkDotNet.Environments; | |
using BenchmarkDotNet.Jobs; | |
using BenchmarkDotNet.Running; | |
namespace SortBench; | |
[Config(typeof(Config))] | |
public class SortBenchTest | |
{ | |
// Partitions below this size are sorted using insertion sort. | |
const int InsertionSortThreshold = 24; | |
// Partitions above this size use Tukey's ninther to select the pivot. | |
const int NintherThreshold = 128; | |
// When we detect an already sorted partition, attempt an insertion sort that allows this | |
// amount of element moves before giving up. | |
const int PartialInsertionSortLimit = 8; | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
static ref T UnguardedAccess<T>(Span<T> span, int index) | |
{ | |
return ref Unsafe.Add(ref MemoryMarshal.GetReference(span), index); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
static Span<T> UnguardedSlice<T>(Span<T> span, int begin, int end) | |
{ | |
return MemoryMarshal.CreateSpan(ref UnguardedAccess(span, begin), end - begin); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
static bool Compare<T>(T left, T right, IComparer<T> comparer) | |
{ | |
return comparer.Compare(left, right) < 0; | |
} | |
static void HeapSort<T>(Span<T> span, IComparer<T> comparer) | |
{ | |
if (span.Length == 0) return; | |
int n = span.Length; | |
for (int i = n >> 1; i >= 1; i--) | |
{ | |
DownHeap(span, i, n, comparer); | |
} | |
for (int i = n; i > 1; i--) | |
{ | |
Swap(ref UnguardedAccess(span, 0), ref UnguardedAccess(span, i - 1)); | |
DownHeap(span, 1, i - 1, comparer); | |
} | |
} | |
static void DownHeap<T>(Span<T> span, int i, int n, IComparer<T> comparer) | |
{ | |
T d = UnguardedAccess(span, i - 1); | |
while (i <= n >> 1) | |
{ | |
int child = 2 * i; | |
if (child < n && Compare(UnguardedAccess(span, child - 1), UnguardedAccess(span, child), comparer)) | |
{ | |
child++; | |
} | |
if (!Compare(d, UnguardedAccess(span, child - 1), comparer)) | |
break; | |
UnguardedAccess(span, i - 1) = UnguardedAccess(span, child - 1); | |
i = child; | |
} | |
UnguardedAccess(span, i - 1) = d; | |
} | |
// Sorts [begin, end) using insertion sort with the given comparison function. | |
static void InsertionSort<T>(Span<T> span, int begin, int end, IComparer<T> comparer) | |
{ | |
if (begin == end) return; | |
for (var i = begin + 1; i < end; i++) | |
{ | |
var sift = i; | |
var sift1 = i - 1; | |
// Compare first so we can avoid 2 moves for an element already positioned correctly. | |
if (Compare(UnguardedAccess(span, sift), UnguardedAccess(span, sift1), comparer)) | |
{ | |
var tmp = UnguardedAccess(span, sift); | |
do | |
{ | |
UnguardedAccess(span, sift--) = UnguardedAccess(span, sift1); | |
} | |
while (sift != begin && Compare(tmp, UnguardedAccess(span, --sift1), comparer)); | |
UnguardedAccess(span, sift) = tmp; | |
} | |
} | |
} | |
// Sorts [begin, end) using insertion sort with the given comparison function. Assumes | |
// *(begin - 1) is an element smaller than or equal to any element in [begin, end). | |
static void UnguardedInsertionSort<T>(Span<T> span, int begin, int end, IComparer<T> comparer) | |
{ | |
if (begin == end) return; | |
for (var i = begin + 1; i < end; i++) | |
{ | |
var sift = i; | |
var sift1 = i - 1; | |
// Compare first so we can avoid 2 moves for an element already positioned correctly. | |
if (Compare(UnguardedAccess(span, sift), UnguardedAccess(span, sift1), comparer)) | |
{ | |
var tmp = UnguardedAccess(span, sift); | |
do | |
{ | |
UnguardedAccess(span, sift--) = UnguardedAccess(span, sift1); | |
} | |
while (Compare(tmp, UnguardedAccess(span, --sift1), comparer)); | |
UnguardedAccess(span, sift) = tmp; | |
} | |
} | |
} | |
// Attempts to use insertion sort on [begin, end). Will return false if more than | |
// partial_insertion_sort_limit elements were moved, and abort sorting. Otherwise it will | |
// successfully sort and return true. | |
static bool PartialInsertionSort<T>(Span<T> span, int begin, int end, IComparer<T> comparer) | |
{ | |
if (begin == end) return true; | |
var limit = 0; | |
for (var i = begin + 1; i < end; i++) | |
{ | |
var sift = i; | |
var sift1 = i - 1; | |
// Compare first so we can avoid 2 moves for an element already positioned correctly. | |
if (Compare(UnguardedAccess(span, sift), UnguardedAccess(span, sift1), comparer)) | |
{ | |
var tmp = UnguardedAccess(span, sift); | |
do | |
{ | |
UnguardedAccess(span, sift--) = UnguardedAccess(span, sift1); | |
} | |
while (sift != begin && Compare(tmp, UnguardedAccess(span, --sift1), comparer)); | |
UnguardedAccess(span, sift) = tmp; | |
limit += i - sift; | |
} | |
if (limit > PartialInsertionSortLimit) return false; | |
} | |
return true; | |
} | |
// Partitions [begin, end) around pivot *begin using comparison function comp. Elements equal | |
// to the pivot are put in the right-hand partition. Returns the position of the pivot after | |
// partitioning and whether the passed sequence already was correctly partitioned. Assumes the | |
// pivot is a median of at least 3 elements and that [begin, end) is at least | |
// insertion_sort_threshold long. | |
static (int Pivot, bool HasPartitioned) PartitionRight<T>(Span<T> span, int begin, int end, IComparer<T> comparer) | |
{ | |
// Move pivot into local for speed. | |
var pivot = UnguardedAccess(span, begin); | |
var first = begin; | |
var last = end; | |
// Find the first element greater than or equal than the pivot (the median of 3 guarantees | |
// this exists). | |
while (Compare(UnguardedAccess(span, ++first), pivot, comparer)) ; | |
// Find the first element strictly smaller than the pivot. We have to guard this search if | |
// there was no element before *first. | |
if (first - 1 == 0) while (first < last && !Compare(UnguardedAccess(span, --last), pivot, comparer)) ; | |
else while (!Compare(UnguardedAccess(span, --last), pivot, comparer)) ; | |
// If the first pair of elements that should be swapped to partition are the same element, | |
// the passed in sequence already was correctly partitioned. | |
bool hasPartitioned = first >= last; | |
// Keep swapping pairs of elements that are on the wrong side of the pivot. Previously | |
// swapped pairs guard the searches, which is why the first iteration is special-cased | |
// above. | |
while (first < last) | |
{ | |
Swap(ref UnguardedAccess(span, first), ref UnguardedAccess(span, last)); | |
while (Compare(UnguardedAccess(span, ++first), pivot, comparer)) ; | |
while (!Compare(UnguardedAccess(span, --last), pivot, comparer)) ; | |
} | |
// Put the pivot in the right place. | |
var pivotPosition = first - 1; | |
UnguardedAccess(span, begin) = UnguardedAccess(span, pivotPosition); | |
UnguardedAccess(span, pivotPosition) = pivot; | |
return (pivotPosition, hasPartitioned); | |
} | |
// Similar function to the one above, except elements equal to the pivot are put to the left of | |
// the pivot and it doesn't check or return if the passed sequence already was partitioned. | |
// Since this is rarely used (the many equal case), and in that case pdqsort already has O(n) | |
// performance, no block quicksort is applied here for simplicity. | |
static int PartitionLeft<T>(Span<T> span, int begin, int end, IComparer<T> comparer) | |
{ | |
var pivot = UnguardedAccess(span, begin); | |
var first = begin; | |
var last = end; | |
while (Compare(pivot, UnguardedAccess(span, --last), comparer)) ; | |
if (last + 1 == end) while (first < last && !Compare(pivot, UnguardedAccess(span, ++first), comparer)) ; | |
else while (!Compare(pivot, UnguardedAccess(span, ++first), comparer)) ; | |
while (first < last) | |
{ | |
Swap(ref UnguardedAccess(span, first), ref UnguardedAccess(span, last)); | |
while (Compare(pivot, UnguardedAccess(span, --last), comparer)) ; | |
while (!Compare(pivot, UnguardedAccess(span, ++first), comparer)) ; | |
} | |
var pivotPosition = last; | |
UnguardedAccess(span, begin) = UnguardedAccess(span, pivotPosition); | |
UnguardedAccess(span, pivotPosition) = pivot; | |
return pivotPosition; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
static void Swap<T>(ref T a, ref T b) | |
{ | |
var t = a; | |
a = b; | |
b = t; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
static void Sort2<T>(ref T a, ref T b, IComparer<T> comparer) | |
{ | |
if (Compare(b, a, comparer)) | |
{ | |
Swap(ref a, ref b); | |
} | |
} | |
// Sorts the elements *a, *b and *c using comparison function comp. | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
static void Sort3<T>(ref T a, ref T b, ref T c, IComparer<T> comparer) | |
{ | |
Sort2(ref a, ref b, comparer); | |
Sort2(ref b, ref c, comparer); | |
Sort2(ref a, ref b, comparer); | |
} | |
static void PdqSort<T>(Span<T> span, int begin, int end, IComparer<T> comparer, int badAllowed, bool leftmost = true) | |
{ | |
while (true) | |
{ | |
var size = end - begin; | |
// Insertion sort is faster for small arrays. | |
if (size < InsertionSortThreshold) | |
{ | |
if (leftmost) InsertionSort(span, begin, end, comparer); | |
else UnguardedInsertionSort(span, begin, end, comparer); | |
return; | |
} | |
// Choose pivot as median of 3 or pseudomedian of 9. | |
var mid = size / 2; | |
if (size > NintherThreshold) | |
{ | |
Sort3(ref UnguardedAccess(span, begin), ref UnguardedAccess(span, begin + mid), ref UnguardedAccess(span, end - 1), comparer); | |
Sort3(ref UnguardedAccess(span, begin + 1), ref UnguardedAccess(span, begin + mid - 1), ref UnguardedAccess(span, end - 2), comparer); | |
Sort3(ref UnguardedAccess(span, begin + 2), ref UnguardedAccess(span, begin + mid + 1), ref UnguardedAccess(span, end - 3), comparer); | |
Sort3(ref UnguardedAccess(span, begin + mid - 1), ref UnguardedAccess(span, begin + mid), ref UnguardedAccess(span, begin + mid + 1), comparer); | |
Swap(ref UnguardedAccess(span, begin), ref UnguardedAccess(span, begin + mid)); | |
} | |
else | |
{ | |
Sort3(ref UnguardedAccess(span, begin + mid), ref UnguardedAccess(span, begin), ref UnguardedAccess(span, end - 1), comparer); | |
} | |
// If *(begin - 1) is the end of the right partition of a previous partition operation | |
// there is no element in [begin, end) that is smaller than *(begin - 1). Then if our | |
// pivot compares equal to *(begin - 1) we change strategy, putting equal elements in | |
// the left partition, greater elements in the right partition. We do not have to | |
// recurse on the left partition, since it's sorted (all equal). | |
if (!leftmost && !Compare(UnguardedAccess(span, begin - 1), UnguardedAccess(span, begin), comparer)) | |
{ | |
begin = PartitionLeft(span, begin, end, comparer) + 1; | |
continue; | |
} | |
var (pivot, hasPartitioned) = PartitionRight(span, begin, end, comparer); | |
// Check for a highly unbalanced partition. | |
var leftSize = pivot - begin; | |
var rightSize = end - (pivot + 1); | |
var highlyUnbalanced = leftSize < size / 8 || rightSize < size / 8; | |
// If we got a highly unbalanced partition we shuffle elements to break many patterns. | |
if (highlyUnbalanced) | |
{ | |
// If we had too many bad partitions, switch to heapsort to guarantee O(n log n). | |
if (--badAllowed == 0) | |
{ | |
HeapSort(UnguardedSlice(span, begin, end), comparer); | |
return; | |
} | |
if (leftSize >= InsertionSortThreshold) | |
{ | |
Swap(ref UnguardedAccess(span, begin), ref UnguardedAccess(span, begin + leftSize / 4)); | |
Swap(ref UnguardedAccess(span, pivot - 1), ref UnguardedAccess(span, pivot - leftSize / 4)); | |
if (leftSize > NintherThreshold) | |
{ | |
Swap(ref UnguardedAccess(span, begin + 1), ref UnguardedAccess(span, begin + leftSize / 4 + 1)); | |
Swap(ref UnguardedAccess(span, begin + 2), ref UnguardedAccess(span, begin + leftSize / 4 + 2)); | |
Swap(ref UnguardedAccess(span, pivot - 2), ref UnguardedAccess(span, pivot - (leftSize / 4 + 1))); | |
Swap(ref UnguardedAccess(span, pivot - 3), ref UnguardedAccess(span, pivot - (leftSize / 4 + 2))); | |
} | |
} | |
if (rightSize >= InsertionSortThreshold) | |
{ | |
Swap(ref UnguardedAccess(span, pivot + 1), ref UnguardedAccess(span, pivot + 1 + rightSize / 4)); | |
Swap(ref UnguardedAccess(span, end - 1), ref UnguardedAccess(span, end - rightSize / 4)); | |
if (rightSize > NintherThreshold) | |
{ | |
Swap(ref UnguardedAccess(span, pivot + 2), ref UnguardedAccess(span, pivot + 2 + rightSize / 4)); | |
Swap(ref UnguardedAccess(span, pivot + 3), ref UnguardedAccess(span, pivot + 3 + rightSize / 4)); | |
Swap(ref UnguardedAccess(span, end - 2), ref UnguardedAccess(span, end - (1 + rightSize / 4))); | |
Swap(ref UnguardedAccess(span, end - 3), ref UnguardedAccess(span, end - (2 + rightSize / 4))); | |
} | |
} | |
} | |
else | |
{ | |
// If we were decently balanced and we tried to sort an already partitioned | |
// sequence try to use insertion sort. | |
if (hasPartitioned && | |
PartialInsertionSort(span, begin, pivot, comparer) && | |
PartialInsertionSort(span, pivot + 1, end, comparer)) | |
{ | |
return; | |
} | |
} | |
// Sort the left partition first using recursion and do tail recursion elimination for | |
// the right-hand partition. | |
PdqSort(span, begin, pivot, comparer, badAllowed, leftmost); | |
begin = pivot + 1; | |
leftmost = false; | |
} | |
} | |
static void Sort<T>(T[] array, IComparer<T> comparer) | |
{ | |
PdqSort(array, 0, array.Length, comparer, BitOperations.Log2((uint)array.Length)); | |
} | |
static IEnumerable<int> GetRandomSequence(int count, bool near) | |
{ | |
while (count-- > 0) yield return near ? Random.Shared.Next(0, count / 4) : Random.Shared.Next(); | |
} | |
static IEnumerable<int> GetReverseSequence(int count) | |
{ | |
while (count-- > 0) yield return count; | |
} | |
static IEnumerable<int> GetOrderSequence(int count) | |
{ | |
var i = 0; | |
while (count-- > 0) yield return i++; | |
} | |
static IEnumerable<int> GetPeekSequence(int count) | |
{ | |
var mid = count / 2; | |
var i = 0; | |
while (mid-- > 0) | |
{ | |
count--; | |
yield return i++; | |
} | |
while (count-- > 0) yield return i--; | |
} | |
static IEnumerable<int> GetReversePeekSequence(int count) | |
{ | |
var mid = count / 2; | |
while (mid-- > 0) | |
{ | |
count--; | |
yield return mid; | |
} | |
var i = 0; | |
while (count-- > 0) yield return i++; | |
} | |
[Params(10, 100, 1000, 10000, 100000, 1000000)] | |
public int size; | |
[GlobalSetup] | |
public void GlobalSetup() | |
{ | |
a_allEqual = new int[size]; | |
p_allEqual = new int[size]; | |
a_randomNear = new int[size]; | |
p_randomNear = new int[size]; | |
a_random = new int[size]; | |
p_random = new int[size]; | |
a_sequence = new int[size]; | |
p_sequence = new int[size]; | |
a_reverse = new int[size]; | |
p_reverse = new int[size]; | |
a_peek = new int[size]; | |
p_peek = new int[size]; | |
a_reversePeek = new int[size]; | |
p_reversePeek = new int[size]; | |
allEqual = Enumerable.Repeat(1, size).ToArray(); | |
randomNear = GetRandomSequence(size, true).ToArray(); | |
random = GetRandomSequence(size, false).ToArray(); | |
sequence = GetOrderSequence(size).ToArray(); | |
reverse = GetReverseSequence(size).ToArray(); | |
peek = GetPeekSequence(size).ToArray(); | |
reversePeek = GetReversePeekSequence(size).ToArray(); | |
} | |
[IterationSetup] | |
public void InitIteration() | |
{ | |
allEqual.CopyTo(a_allEqual, 0); | |
allEqual.CopyTo(p_allEqual, 0); | |
random.CopyTo(a_random, 0); | |
random.CopyTo(p_random, 0); | |
randomNear.CopyTo(a_randomNear, 0); | |
randomNear.CopyTo(p_randomNear, 0); | |
sequence.CopyTo(a_sequence, 0); | |
sequence.CopyTo(p_sequence, 0); | |
reverse.CopyTo(a_reverse, 0); | |
reverse.CopyTo(p_reverse, 0); | |
peek.CopyTo(a_peek, 0); | |
peek.CopyTo(p_peek, 0); | |
reversePeek.CopyTo(a_reversePeek, 0); | |
reversePeek.CopyTo(p_reversePeek, 0); | |
} | |
private int[] allEqual, randomNear, random, sequence, reverse, peek, reversePeek; | |
private int[] a_allEqual, a_randomNear, a_random, a_sequence, a_reverse, a_peek, a_reversePeek; | |
private int[] p_allEqual, p_randomNear, p_random, p_sequence, p_reverse, p_peek, p_reversePeek; | |
class Config : ManualConfig | |
{ | |
public Config() | |
{ | |
AddJob(Job.Default | |
.WithMaxIterationCount(10000) | |
.WithMinWarmupCount(2000) | |
.WithMaxWarmupCount(10000) | |
.WithEnvironmentVariable("DOTNET_TieredPgo", "1") | |
.WithRuntime(CoreRuntime.CreateForNewVersion("net7.0", ".NET 7.0"))); | |
} | |
} | |
static void Main(string[] args) | |
{ | |
BenchmarkRunner.Run<SortBenchTest>(null, args); | |
} | |
struct MyComparer<T> : IComparer<T> | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public int Compare(T? x, T? y) | |
{ | |
return Comparer<T>.Default.Compare(x, y); | |
} | |
} | |
[Benchmark] | |
public void ArraySort_AllEqual() | |
{ | |
Array.Sort(a_allEqual, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void PdqSort_AllEqual() | |
{ | |
Sort(p_allEqual, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void ArraySort_RandomNear() | |
{ | |
Array.Sort(a_randomNear, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void PdqSort_RandomNear() | |
{ | |
Sort(p_randomNear, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void ArraySort_Random() | |
{ | |
Array.Sort(a_random, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void PdqSort_Random() | |
{ | |
Sort(p_random, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void ArraySort_Sequence() | |
{ | |
Array.Sort(a_sequence, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void PdqSort_Sequence() | |
{ | |
Sort(p_sequence, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void ArraySort_ReverseSequence() | |
{ | |
Array.Sort(a_reverse, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void PdqSort_ReverseSequence() | |
{ | |
Sort(p_reverse, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void ArraySort_Peek() | |
{ | |
Array.Sort(a_peek, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void PdqSort_Peek() | |
{ | |
Sort(p_peek, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void ArraySort_ReversePeek() | |
{ | |
Array.Sort(a_reversePeek, new MyComparer<int>()); | |
} | |
[Benchmark] | |
public void PdqSort_ReversePeek() | |
{ | |
Sort(p_reversePeek, new MyComparer<int>()); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Benchmark result (.NET 7 with PGO+QJFL+OSR enabled):