From 2b23a719a32057f34a4aaf53a949cd42b4614726 Mon Sep 17 00:00:00 2001 From: Henrique Ruschel Date: Thu, 16 Oct 2025 04:28:12 -0300 Subject: [PATCH 1/2] Packing keys on EnumerableSorter --- .../src/System/Linq/OrderedEnumerable.cs | 279 +++++++++++++++++- .../System.Linq/tests/OrderByTests.cs | 138 +++++++++ 2 files changed, 414 insertions(+), 3 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs index 447094cc867f9e..735b3ca6ecc5f3 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs @@ -4,6 +4,9 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using System.Runtime.CompilerServices; namespace System.Linq { @@ -101,7 +104,17 @@ internal override EnumerableSorter GetEnumerableSorter(EnumerableSorte comparer = (IComparer)StringComparer.CurrentCulture; } - EnumerableSorter sorter = new EnumerableSorter(_keySelector, comparer, _descending, next); + EnumerableSorter sorter; + + if (next is null) + { + sorter = new EnumerableSorter(_keySelector, comparer, _descending, next); + } + else + { + sorter = next.CreateWithChild(_keySelector, comparer, _descending); + } + if (_parent is not null) { sorter = _parent.GetEnumerableSorter(sorter); @@ -122,7 +135,7 @@ public override bool MoveNext() { int state = _state; - Initialized: + Initialized: if (state > 1) { Debug.Assert(_buffer is not null); @@ -195,7 +208,7 @@ public override bool MoveNext() int state = _state; TElement[]? buffer; - Initialized: + Initialized: if (state > 1) { buffer = _buffer; @@ -343,6 +356,8 @@ private int[] ComputeMap(TElement[] elements, int count) return map; } + internal abstract EnumerableSorter CreateWithChild(Func keySelector, IComparer comparer, bool descending); + internal int[] Sort(TElement[] elements, int count) { int[] map = ComputeMap(elements, count); @@ -385,6 +400,7 @@ private sealed class EnumerableSorter : EnumerableSorter? _next; private TKey[]? _keys; + private readonly byte _packingUsedSize; internal EnumerableSorter(Func keySelector, IComparer comparer, bool descending, EnumerableSorter? next) { @@ -394,6 +410,31 @@ internal EnumerableSorter(Func keySelector, IComparer comp _next = next; } + private EnumerableSorter(Func keySelector, IComparer comparer, bool descending, EnumerableSorter? next, byte packingUsedSize) + { + _keySelector = keySelector; + _comparer = comparer; + _descending = descending; + _next = next; + _packingUsedSize = packingUsedSize; + } + + internal override EnumerableSorter CreateWithChild(Func keySelector, IComparer comparer, bool descending) + { + EnumerableSorter sorter; + + if (TryPackKeys(keySelector, comparer, descending, this, out var packedSorter)) + { + sorter = packedSorter; + } + else + { + sorter = new EnumerableSorter(keySelector, comparer, descending, this); + } + + return sorter; + } + internal override void ComputeKeys(TElement[] elements, int count) { Func keySelector = _keySelector; @@ -653,6 +694,238 @@ protected override int Min(int[] map, int count) } return map[index]; } + + private static bool TryPackKeys(Func keySelector, IComparer comparer, bool descending, EnumerableSorter next, [NotNullWhen(true)] out EnumerableSorter? packedSorter) + { + bool key1typeIsSigned; + bool key2typeIsSigned; + int key1typeSize; + int key2typeSize; + + if (!TryGetPackingTypeData(out key1typeIsSigned, out key1typeSize) || + !TryGetPackingTypeData(out key2typeIsSigned, out key2typeSize) || + comparer != Comparer.Default || next._comparer != Comparer.Default) + { + packedSorter = null; + return false; + } + + int packingSize = next._packingUsedSize == 0 ? key2typeSize : next._packingUsedSize; + + byte totalSize = (byte)(key1typeSize + packingSize); + + if (totalSize <= sizeof(uint)) + { + uint toggle = 0U; + if (key1typeIsSigned) + { + toggle |= 1U << ((totalSize * 8) - 1); + } + if (key2typeIsSigned) + { + toggle |= 1U << ((key2typeSize * 8) - 1); + } + packedSorter = CreatePacked( + highKeySelector: keySelector, + lowKeySelector: next._keySelector, + key2typeSize: packingSize, + totalSize: totalSize, + key1IsDescending: descending, + key2IsDescending: next._descending, + toggleSignBits: toggle, + next: next?._next + ); + return true; + } + + if (totalSize <= sizeof(ulong)) + { + ulong toggle = 0UL; + if (key1typeIsSigned) + { + toggle |= 1UL << ((totalSize * 8) - 1); + } + if (key2typeIsSigned) + { + toggle |= 1UL << ((key2typeSize * 8) - 1); + } + packedSorter = CreatePacked( + highKeySelector: keySelector, + lowKeySelector: next._keySelector, + key2typeSize: packingSize, + totalSize: totalSize, + key1IsDescending: descending, + key2IsDescending: next._descending, + toggleSignBits: toggle, + next: next?._next + ); + return true; + } + + packedSorter = null; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool TryGetPackingTypeData(out bool isSigned, out int size) + { + if (typeof(T) == typeof(sbyte)) + { + isSigned = true; + size = sizeof(sbyte); + return true; + } + if (typeof(T) == typeof(short)) + { + isSigned = true; + size = sizeof(short); + return true; + } + if (typeof(T) == typeof(int)) + { + isSigned = true; + size = sizeof(int); + return true; + } + if (typeof(T) == typeof(nint)) + { + isSigned = true; + size = Unsafe.SizeOf(); + return true; + } + if (typeof(T) == typeof(long)) + { + isSigned = true; + size = sizeof(long); + return true; + } + + if (typeof(T) == typeof(byte)) + { + isSigned = false; + size = sizeof(byte); + return true; + } + if (typeof(T) == typeof(ushort)) + { + isSigned = false; + size = sizeof(ushort); + return true; + } + if (typeof(T) == typeof(uint)) + { + isSigned = false; + size = sizeof(uint); + return true; + } + if (typeof(T) == typeof(nuint)) + { + isSigned = false; + size = Unsafe.SizeOf(); + return true; + } + if (typeof(T) == typeof(ulong)) + { + isSigned = false; + size = sizeof(ulong); + return true; + } + + if (typeof(T) == typeof(char)) + { + isSigned = false; + size = sizeof(char); + return true; + } + + if (typeof(T) == typeof(bool)) + { + isSigned = false; + size = sizeof(bool); + return true; + } + + isSigned = false; + size = default; + + return false; + } + + private static EnumerableSorter CreatePacked( + Func highKeySelector, + Func lowKeySelector, + int key2typeSize, + byte totalSize, + bool key1IsDescending, + bool key2IsDescending, + TNewKey toggleSignBits, + EnumerableSorter? next + ) + where TNewKey : IBitwiseOperators, IShiftOperators, IUnsignedNumber + { + // see github.com/dotnet/runtime/issues/120785 for more information + if (key1IsDescending != key2IsDescending) + { + // make the parent order not apply to the child + TNewKey toggleLowKeyOrder = (TNewKey.One << key2typeSize * 8) - TNewKey.One; + toggleSignBits ^= toggleLowKeyOrder; + } + + if (toggleSignBits == TNewKey.Zero) + { + //result ^= 0 does nothing + return new EnumerableSorter(x => + { + TKey1 highKey = highKeySelector(x); + TKey2 lowKey = lowKeySelector(x); + + TNewKey result = default!; + + Unsafe.WriteUnaligned(ref Unsafe.As(ref result), lowKey); + + ref byte dest = ref Unsafe.Add(ref Unsafe.As(ref result), key2typeSize); + Unsafe.WriteUnaligned(ref dest, highKey); + + return result; + }, Comparer.Default, key1IsDescending, next, totalSize); + } + + return new EnumerableSorter(x => + { + // highKey = 11111111 + TKey1 highKey = highKeySelector(x); + // lowKey = 01111000 + TKey2 lowKey = lowKeySelector(x); + + // result = 00000000_00000000 + TNewKey result = default!; + + // WriteUnaligned will write the bits in the low end of result + Unsafe.WriteUnaligned(ref Unsafe.As(ref result), lowKey); + // result = 00000000_01111000 + // |--lo--| + + // now we want to skip the size of the lowKey writted in the result + ref byte dest = ref Unsafe.Add(ref Unsafe.As(ref result), key2typeSize); + // |--hi--| |--lo--| + // 00000000_01111000 + // ^ now we are here + + // Write the highKey after lowKey + Unsafe.WriteUnaligned(ref dest, highKey); + // result = 11111111_01111000 + // |--hi--| |--lo--| + // toggle the sign bit, to safe convert to unsigned + // (sbyte)0 in binary 00000000 will be 10000000, now (sbyte)0 is in the middle + // basically doing this: + // Middle + // MinValue|----------|----------|MaxValue + // 0 + result ^= toggleSignBits; + + return result; + }, Comparer.Default, key1IsDescending, next, totalSize); + } } } } diff --git a/src/libraries/System.Linq/tests/OrderByTests.cs b/src/libraries/System.Linq/tests/OrderByTests.cs index e711be707861dd..9009c5ae66679f 100644 --- a/src/libraries/System.Linq/tests/OrderByTests.cs +++ b/src/libraries/System.Linq/tests/OrderByTests.cs @@ -633,5 +633,143 @@ public void OrderBy_FirstLast_MatchesArray() Assert.Same(objects.OrderBy(x => x).Last(), objects.OrderBy(x => x).ToArray().Last()); } } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Packed_Ascending_Ascending_Should_Order_Correctly(int zeroOrders) + { + var expected = new int[] + { + 0, 1, 2, 3, 4, 5, + -5, -4, -3, -2, -1, + 100, 101, 102, 103, 104, 105, + }; + + var source = expected.Shuffle().ToArray(); + + var ordered = zeroOrders switch + { + 0 => source.OrderBy(x => x.ToString().Length).ThenBy(x => x), + + 1 => source.OrderBy(_ => 0) + .ThenBy(x => x.ToString().Length).ThenBy(x => x), + + 2 => source.OrderBy(_ => 0).ThenBy(_ => 0) + .ThenBy(x => x.ToString().Length).ThenBy(x => x), + + 3 => source.OrderBy(_ => 0).ThenBy(_ => 0).ThenBy(_ => 0) + .ThenBy(x => x.ToString().Length).ThenBy(x => x), + + _ => throw new NotImplementedException(), + }; + + Assert.Equal(expected, ordered); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Packed_Ascending_Descending_Should_Order_Correctly(int zeroOrders) + { + var expected = new int[] + { + 5, 4, 3, 2, 1, 0, + -1, -2, -3, -4, -5, + 105, 104, 103, 102, 101, 100, + }; + + var source = expected.Shuffle().ToArray(); + + var ordered = zeroOrders switch + { + 0 => source.OrderBy(x => x.ToString().Length).ThenByDescending(x => x), + + 1 => source.OrderBy(_ => 0) + .ThenBy(x => x.ToString().Length).ThenByDescending(x => x), + + 2 => source.OrderBy(_ => 0).ThenBy(_ => 0) + .ThenBy(x => x.ToString().Length).ThenByDescending(x => x), + + 3 => source.OrderBy(_ => 0).ThenBy(_ => 0).ThenBy(_ => 0) + .ThenBy(x => x.ToString().Length).ThenByDescending(x => x), + + _ => throw new NotImplementedException(), + }; + + Assert.Equal(expected, ordered); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Packed_Descending_Ascending_Should_Order_Correctly(int zeroOrders) + { + var expected = new int[] + { + 100, 101, 102, 103, 104, 105, + -5, -4, -3, -2, -1, + 0, 1, 2, 3, 4, 5, + }; + + var source = expected.Shuffle().ToArray(); + + var ordered = zeroOrders switch + { + 0 => source.OrderByDescending(x => x.ToString().Length).ThenBy(x => x), + + 1 => source.OrderBy(_ => 0) + .ThenByDescending(x => x.ToString().Length).ThenBy(x => x), + + 2 => source.OrderBy(_ => 0).ThenBy(_ => 0) + .ThenByDescending(x => x.ToString().Length).ThenBy(x => x), + + 3 => source.OrderBy(_ => 0).ThenBy(_ => 0).ThenBy(_ => 0) + .ThenByDescending(x => x.ToString().Length).ThenBy(x => x), + + _ => throw new NotImplementedException(), + }; + Assert.Equal(expected, ordered); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void Packed_Descending_Descending_Should_Order_Correctly(int zeroOrders) + { + var expected = new int[] + { + 105, 104, 103, 102, 101, 100, + -1, -2, -3, -4, -5, + 5, 4, 3, 2, 1, 0, + }; + + var source = expected.Shuffle().ToArray(); + + var ordered = zeroOrders switch + { + 0 => source.OrderByDescending(x => x.ToString().Length).ThenByDescending(x => x), + + 1 => source.OrderBy(_ => 0) + .ThenByDescending(x => x.ToString().Length).ThenByDescending(x => x), + + 2 => source.OrderBy(_ => 0).ThenBy(_ => 0) + .ThenByDescending(x => x.ToString().Length).ThenByDescending(x => x), + + 3 => source.OrderBy(_ => 0).ThenBy(_ => 0).ThenBy(_ => 0) + .ThenByDescending(x => x.ToString().Length).ThenByDescending(x => x), + + _ => throw new NotImplementedException(), + }; + Assert.Equal(expected, ordered); + } } } From 2544a9a4d77eb92bae643eb2e3f382d9fefce730 Mon Sep 17 00:00:00 2001 From: Henrique Ruschel Date: Tue, 28 Oct 2025 21:56:21 -0300 Subject: [PATCH 2/2] Big endian impl --- .../src/System/Linq/OrderedEnumerable.cs | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs index 735b3ca6ecc5f3..4f174f64c9480f 100644 --- a/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs +++ b/src/libraries/System.Linq/src/System/Linq/OrderedEnumerable.cs @@ -714,6 +714,8 @@ private static bool TryPackKeys(Func keySelector, byte totalSize = (byte)(key1typeSize + packingSize); + int keyPadding = BitConverter.IsLittleEndian ? packingSize : key1typeSize; + if (totalSize <= sizeof(uint)) { uint toggle = 0U; @@ -728,7 +730,7 @@ private static bool TryPackKeys(Func keySelector, packedSorter = CreatePacked( highKeySelector: keySelector, lowKeySelector: next._keySelector, - key2typeSize: packingSize, + keyPadding: keyPadding, totalSize: totalSize, key1IsDescending: descending, key2IsDescending: next._descending, @@ -752,7 +754,7 @@ private static bool TryPackKeys(Func keySelector, packedSorter = CreatePacked( highKeySelector: keySelector, lowKeySelector: next._keySelector, - key2typeSize: packingSize, + keyPadding: keyPadding, totalSize: totalSize, key1IsDescending: descending, key2IsDescending: next._descending, @@ -854,7 +856,7 @@ private static bool TryGetPackingTypeData(out bool isSigned, out int size) private static EnumerableSorter CreatePacked( Func highKeySelector, Func lowKeySelector, - int key2typeSize, + int keyPadding, byte totalSize, bool key1IsDescending, bool key2IsDescending, @@ -867,7 +869,7 @@ private static EnumerableSorter CreatePacked CreatePacked(ref result), lowKey); + ref byte resultByteRef = ref Unsafe.As(ref result); - ref byte dest = ref Unsafe.Add(ref Unsafe.As(ref result), key2typeSize); - Unsafe.WriteUnaligned(ref dest, highKey); + if (BitConverter.IsLittleEndian) + { + Unsafe.WriteUnaligned(ref resultByteRef, lowKey); + + ref byte dest = ref Unsafe.Add(ref resultByteRef, keyPadding); + Unsafe.WriteUnaligned(ref dest, highKey); + } + else + { + Unsafe.WriteUnaligned(ref resultByteRef, highKey); + + ref byte dest = ref Unsafe.Add(ref resultByteRef, keyPadding); + Unsafe.WriteUnaligned(ref dest, lowKey); + } return result; }, Comparer.Default, key1IsDescending, next, totalSize); @@ -900,27 +914,40 @@ private static EnumerableSorter CreatePacked(ref result), lowKey); - // result = 00000000_01111000 - // |--lo--| - - // now we want to skip the size of the lowKey writted in the result - ref byte dest = ref Unsafe.Add(ref Unsafe.As(ref result), key2typeSize); - // |--hi--| |--lo--| - // 00000000_01111000 - // ^ now we are here - - // Write the highKey after lowKey - Unsafe.WriteUnaligned(ref dest, highKey); - // result = 11111111_01111000 - // |--hi--| |--lo--| - // toggle the sign bit, to safe convert to unsigned - // (sbyte)0 in binary 00000000 will be 10000000, now (sbyte)0 is in the middle - // basically doing this: - // Middle - // MinValue|----------|----------|MaxValue - // 0 + ref byte resultByteRef = ref Unsafe.As(ref result); + + if (BitConverter.IsLittleEndian) + { + // WriteUnaligned will write the bits in the low end of result + Unsafe.WriteUnaligned(ref resultByteRef, lowKey); + // result = 00000000_01111000 + // |--lo--| + + // now we want to skip the size of the lowKey writted in the result + ref byte dest = ref Unsafe.Add(ref resultByteRef, keyPadding); + // |--hi--| |--lo--| + // 00000000_01111000 + // ^ now we are here + + // Write the highKey after lowKey + Unsafe.WriteUnaligned(ref dest, highKey); + // result = 11111111_01111000 + // |--hi--| |--lo--| + // toggle the sign bit, to safe convert to unsigned + // (sbyte)0 in binary 00000000 will be 10000000, now (sbyte)0 is in the middle + // basically doing this: + // Middle + // MinValue|----------|----------|MaxValue + // 0 + } + else + { + Unsafe.WriteUnaligned(ref resultByteRef, highKey); + + ref byte dest = ref Unsafe.Add(ref resultByteRef, keyPadding); + Unsafe.WriteUnaligned(ref dest, lowKey); + } + result ^= toggleSignBits; return result;