Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 7931769

Browse files
authored
Add Span SequenceCompareTo extension method (#26232)
* Add SequenceCompareTo initial * Update tests and remove use of Dangerous API * Add SequenceCompareTo performance test and loop unroll * Revert loop unrolling SequenceCompareTo * Vectorize SequenceCompareTo<byte> * Fix performance test class name. * Address PR feedback
1 parent 98ef28c commit 7931769

File tree

13 files changed

+633
-12
lines changed

13 files changed

+633
-12
lines changed

src/System.Memory/ref/System.Memory.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,9 @@ public static class MemoryExtensions
155155
public static bool Overlaps<T>(this ReadOnlySpan<T> first, ReadOnlySpan<T> second) { throw null; }
156156
public static bool Overlaps<T>(this ReadOnlySpan<T> first, ReadOnlySpan<T> second, out int elementOffset) { throw null; }
157157

158+
public static int SequenceCompareTo<T>(this Span<T> first, ReadOnlySpan<T> second) where T : IComparable<T> { throw null; }
159+
public static int SequenceCompareTo<T>(this ReadOnlySpan<T> first, ReadOnlySpan<T> second) where T : IComparable<T> { throw null; }
160+
158161
public static int BinarySearch<T>(this ReadOnlySpan<T> span, IComparable<T> comparable) { throw null; }
159162
public static int BinarySearch<T, TComparable>(this ReadOnlySpan<T> span, TComparable comparable) where TComparable : IComparable<T> { throw null; }
160163
public static int BinarySearch<T, TComparer>(this ReadOnlySpan<T> span, T value, TComparer comparer) where TComparer : IComparer<T> { throw null; }

src/System.Memory/src/System/Buffers/Text/Utf8Parser/Utf8Parser.Guid.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,7 @@ private static bool TryParseGuidN(ReadOnlySpan<byte> text, out Guid value, out i
5555
return false;
5656
}
5757

58-
int justConsumed;
59-
if (!TryParseUInt32X(text.Slice(0, 8), out uint i1, out justConsumed) || justConsumed != 8)
58+
if (!TryParseUInt32X(text.Slice(0, 8), out uint i1, out int justConsumed) || justConsumed != 8)
6059
{
6160
value = default;
6261
bytesConsumed = 0;
@@ -121,8 +120,7 @@ private static bool TryParseGuidCore(ReadOnlySpan<byte> text, bool ends, char be
121120
text = text.Slice(1); // skip begining
122121
}
123122

124-
int justConsumed;
125-
if (!TryParseUInt32X(text, out uint i1, out justConsumed))
123+
if (!TryParseUInt32X(text, out uint i1, out int justConsumed))
126124
{
127125
value = default;
128126
bytesConsumed = 0;

src/System.Memory/src/System/Buffers/Text/Utf8Parser/Utf8Parser.TimeSpan.BigG.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ private static bool TryParseTimeSpanBigG(ReadOnlySpan<byte> text, out TimeSpan v
3838
}
3939
}
4040

41-
int justConsumed;
42-
if (!TryParseUInt32D(text.Slice(srcIndex), out uint days, out justConsumed))
41+
if (!TryParseUInt32D(text.Slice(srcIndex), out uint days, out int justConsumed))
4342
{
4443
value = default;
4544
bytesConsumed = 0;

src/System.Memory/src/System/MemoryExtensions.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,21 @@ ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(second)),
104104
return length == second.Length && SpanHelpers.SequenceEqual(ref MemoryMarshal.GetReference(first), ref MemoryMarshal.GetReference(second), length);
105105
}
106106

107+
/// <summary>
108+
/// Determines the relative order of the sequences being compared by comparing the elements using IComparable{T}.CompareTo(T).
109+
/// </summary>
110+
public static int SequenceCompareTo<T>(this Span<T> first, ReadOnlySpan<T> second)
111+
where T : IComparable<T>
112+
{
113+
if (typeof(T) == typeof(byte))
114+
return SpanHelpers.SequenceCompareTo(
115+
ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(first)),
116+
first.Length,
117+
ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(second)),
118+
second.Length);
119+
return SpanHelpers.SequenceCompareTo(ref MemoryMarshal.GetReference(first), first.Length, ref MemoryMarshal.GetReference(second), second.Length);
120+
}
121+
107122
/// <summary>
108123
/// Searches for the specified value and returns the index of its first occurrence. If not found, returns -1. Values are compared using IEquatable{T}.Equals(T).
109124
/// </summary>
@@ -429,6 +444,21 @@ ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(second)),
429444
return length == second.Length && SpanHelpers.SequenceEqual(ref MemoryMarshal.GetReference(first), ref MemoryMarshal.GetReference(second), length);
430445
}
431446

447+
/// <summary>
448+
/// Determines the relative order of the sequences being compared by comparing the elements using IComparable{T}.CompareTo(T).
449+
/// </summary>
450+
public static int SequenceCompareTo<T>(this ReadOnlySpan<T> first, ReadOnlySpan<T> second)
451+
where T : IComparable<T>
452+
{
453+
if (typeof(T) == typeof(byte))
454+
return SpanHelpers.SequenceCompareTo(
455+
ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(first)),
456+
first.Length,
457+
ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(second)),
458+
second.Length);
459+
return SpanHelpers.SequenceCompareTo(ref MemoryMarshal.GetReference(first), first.Length, ref MemoryMarshal.GetReference(second), second.Length);
460+
}
461+
432462
/// <summary>
433463
/// Determines whether the specified sequence appears at the start of the span.
434464
/// </summary>

src/System.Memory/src/System/SpanHelpers.T.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,5 +663,21 @@ public static bool SequenceEqual<T>(ref T first, ref T second, int length)
663663
NotEqual: // Workaround for https://github.com/dotnet/coreclr/issues/13549
664664
return false;
665665
}
666+
667+
public static int SequenceCompareTo<T>(ref T first, int firstLength, ref T second, int secondLength)
668+
where T : IComparable<T>
669+
{
670+
Debug.Assert(firstLength >= 0);
671+
Debug.Assert(secondLength >= 0);
672+
673+
var minLength = firstLength;
674+
if (minLength > secondLength) minLength = secondLength;
675+
for (int i = 0; i < minLength; i++)
676+
{
677+
int result = Unsafe.Add(ref first, i).CompareTo(Unsafe.Add(ref second, i));
678+
if (result != 0) return result;
679+
}
680+
return firstLength.CompareTo(secondLength);
681+
}
666682
}
667683
}

src/System.Memory/src/System/SpanHelpers.byte.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -967,6 +967,63 @@ private static int LocateFirstFoundByte(Vector<byte> match)
967967
}
968968
#endif
969969

970+
public static unsafe int SequenceCompareTo(ref byte first, int firstLength, ref byte second, int secondLength)
971+
{
972+
Debug.Assert(firstLength >= 0);
973+
Debug.Assert(secondLength >= 0);
974+
975+
if (Unsafe.AreSame(ref first, ref second))
976+
goto Equal;
977+
978+
var minLength = firstLength;
979+
if (minLength > secondLength) minLength = secondLength;
980+
981+
IntPtr i = (IntPtr)0; // Use IntPtr and byte* for arithmetic to avoid unnecessary 64->32->64 truncations
982+
IntPtr n = (IntPtr)minLength;
983+
984+
#if !netstandard11
985+
if (Vector.IsHardwareAccelerated && (byte*)n > (byte*)Vector<byte>.Count)
986+
{
987+
n -= Vector<byte>.Count;
988+
while ((byte*)n > (byte*)i)
989+
{
990+
if (Unsafe.ReadUnaligned<Vector<byte>>(ref Unsafe.AddByteOffset(ref first, i)) !=
991+
Unsafe.ReadUnaligned<Vector<byte>>(ref Unsafe.AddByteOffset(ref second, i)))
992+
{
993+
goto NotEqual;
994+
}
995+
i += Vector<byte>.Count;
996+
}
997+
goto NotEqual;
998+
}
999+
#endif
1000+
1001+
if ((byte*)n > (byte*)sizeof(UIntPtr))
1002+
{
1003+
n -= sizeof(UIntPtr);
1004+
while ((byte*)n > (byte*)i)
1005+
{
1006+
if (Unsafe.ReadUnaligned<UIntPtr>(ref Unsafe.AddByteOffset(ref first, i)) !=
1007+
Unsafe.ReadUnaligned<UIntPtr>(ref Unsafe.AddByteOffset(ref second, i)))
1008+
{
1009+
goto NotEqual;
1010+
}
1011+
i += sizeof(UIntPtr);
1012+
}
1013+
}
1014+
1015+
NotEqual: // Workaround for https://github.com/dotnet/coreclr/issues/13549
1016+
while((byte*)minLength > (byte*)i)
1017+
{
1018+
int result = Unsafe.AddByteOffset(ref first, i).CompareTo(Unsafe.AddByteOffset(ref second, i));
1019+
if (result != 0) return result;
1020+
i += 1;
1021+
}
1022+
1023+
Equal:
1024+
return firstLength - secondLength;
1025+
}
1026+
9701027
#if !netstandard11
9711028
// Vector sub-search adapted from https://github.com/aspnet/KestrelHttpServer/pull/1138
9721029
[MethodImpl(MethodImplOptions.AggressiveInlining)]
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using Microsoft.Xunit.Performance;
6+
using Xunit;
7+
8+
namespace System.Memory.Tests
9+
{
10+
public class Perf_Span_SequenceCompareTo
11+
{
12+
private const int InnerCount = 100000;
13+
14+
[Benchmark(InnerIterationCount = InnerCount)]
15+
[InlineData(1)]
16+
[InlineData(10)]
17+
[InlineData(100)]
18+
[InlineData(1000)]
19+
[InlineData(10000)]
20+
public void SequenceCompareToSame_Byte(int size)
21+
{
22+
Span<byte> first = new byte[size];
23+
Span<byte> second = new byte[size];
24+
int result = -1;
25+
26+
foreach (BenchmarkIteration iteration in Benchmark.Iterations)
27+
{
28+
using (iteration.StartMeasurement())
29+
{
30+
for (int i = 0; i < Benchmark.InnerIterationCount; i++)
31+
{
32+
result = first.SequenceCompareTo<byte>(second);
33+
}
34+
}
35+
}
36+
37+
Assert.Equal(0, result);
38+
}
39+
40+
[Benchmark(InnerIterationCount = InnerCount)]
41+
[InlineData(1)]
42+
[InlineData(10)]
43+
[InlineData(100)]
44+
[InlineData(1000)]
45+
[InlineData(10000)]
46+
public void SequenceCompareToDifferent_Byte(int size)
47+
{
48+
Span<byte> first = new byte[size];
49+
Span<byte> second = new byte[size];
50+
int result = -1;
51+
52+
first[size/2] = 1;
53+
54+
foreach (BenchmarkIteration iteration in Benchmark.Iterations)
55+
{
56+
using (iteration.StartMeasurement())
57+
{
58+
for (int i = 0; i < Benchmark.InnerIterationCount; i++)
59+
{
60+
result = first.SequenceCompareTo<byte>(second);
61+
}
62+
}
63+
}
64+
65+
Assert.Equal(1, result);
66+
}
67+
68+
[Benchmark(InnerIterationCount = InnerCount)]
69+
[InlineData(1)]
70+
[InlineData(10)]
71+
[InlineData(100)]
72+
[InlineData(1000)]
73+
[InlineData(10000)]
74+
public void SequenceCompareToSame_Int(int size)
75+
{
76+
Span<int> first = new int[size];
77+
Span<int> second = new int[size];
78+
int result = -1;
79+
80+
foreach (BenchmarkIteration iteration in Benchmark.Iterations)
81+
{
82+
using (iteration.StartMeasurement())
83+
{
84+
for (int i = 0; i < Benchmark.InnerIterationCount; i++)
85+
{
86+
result = first.SequenceCompareTo<int>(second);
87+
}
88+
}
89+
}
90+
91+
Assert.Equal(0, result);
92+
}
93+
94+
[Benchmark(InnerIterationCount = InnerCount)]
95+
[InlineData(1)]
96+
[InlineData(10)]
97+
[InlineData(100)]
98+
[InlineData(1000)]
99+
[InlineData(10000)]
100+
public void SequenceCompareToDifferent_Int(int size)
101+
{
102+
Span<int> first = new int[size];
103+
Span<int> second = new int[size];
104+
int result = -1;
105+
106+
first[size/2] = 1;
107+
108+
foreach (BenchmarkIteration iteration in Benchmark.Iterations)
109+
{
110+
using (iteration.StartMeasurement())
111+
{
112+
for (int i = 0; i < Benchmark.InnerIterationCount; i++)
113+
{
114+
result = first.SequenceCompareTo<int>(second);
115+
}
116+
}
117+
}
118+
119+
Assert.Equal(1, result);
120+
}
121+
122+
[Benchmark(InnerIterationCount = InnerCount)]
123+
[InlineData(1)]
124+
[InlineData(10)]
125+
[InlineData(100)]
126+
[InlineData(1000)]
127+
[InlineData(10000)]
128+
public void SequenceCompareToSame_String(int size)
129+
{
130+
var firstStringArray = new string[size];
131+
var secondStringArray = new string[size];
132+
for (int i = 0; i < size; i++)
133+
{
134+
firstStringArray[i] = secondStringArray[i] = "0";
135+
}
136+
137+
Span<string> first = firstStringArray;
138+
Span<string> second = secondStringArray;
139+
int result = -1;
140+
141+
foreach (BenchmarkIteration iteration in Benchmark.Iterations)
142+
{
143+
using (iteration.StartMeasurement())
144+
{
145+
for (int i = 0; i < Benchmark.InnerIterationCount; i++)
146+
{
147+
result = first.SequenceCompareTo(second);
148+
}
149+
}
150+
}
151+
152+
Assert.Equal(0, result);
153+
}
154+
155+
[Benchmark(InnerIterationCount = InnerCount)]
156+
[InlineData(1)]
157+
[InlineData(10)]
158+
[InlineData(100)]
159+
[InlineData(1000)]
160+
[InlineData(10000)]
161+
public void SequenceCompareToDifferent_String(int size)
162+
{
163+
var firstStringArray = new string[size];
164+
var secondStringArray = new string[size];
165+
for (int i = 0; i < size; i++)
166+
{
167+
firstStringArray[i] = secondStringArray[i] = "0";
168+
}
169+
170+
Span<string> first = firstStringArray;
171+
Span<string> second = secondStringArray;
172+
int result = -1;
173+
174+
first[size/2] = "1";
175+
176+
foreach (BenchmarkIteration iteration in Benchmark.Iterations)
177+
{
178+
using (iteration.StartMeasurement())
179+
{
180+
for (int i = 0; i < Benchmark.InnerIterationCount; i++)
181+
{
182+
result = first.SequenceCompareTo(second);
183+
}
184+
}
185+
}
186+
187+
Assert.Equal(1, result);
188+
}
189+
}
190+
}

src/System.Memory/tests/Performance/System.Memory.Performance.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="Perf.Span.Fill.cs" />
1616
<Compile Include="Perf.Span.IndexOf.cs" />
1717
<Compile Include="Perf.Span.IndexOfAny.cs" />
18+
<Compile Include="Perf.Span.SequenceCompareTo.cs" />
1819
<Compile Include="Perf.Span.StartsWith.cs" />
1920
<Compile Include="Perf.MemorySlice.cs" />
2021
<Compile Include="Perf.Utf8Formatter.cs" />

src/System.Memory/tests/ReadOnlyMemory/Strings.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,7 @@ public static void AsReadOnlyMemory_TryGetString_Roundtrips()
6666
ReadOnlyMemory<char> m = input.AsReadOnlyMemory();
6767
Assert.False(m.IsEmpty);
6868

69-
string text;
70-
int start;
71-
int length;
72-
73-
Assert.True(m.TryGetString(out text, out start, out length));
69+
Assert.True(m.TryGetString(out string text, out int start, out int length));
7470
Assert.Same(input, text);
7571
Assert.Equal(0, start);
7672
Assert.Equal(input.Length, length);

0 commit comments

Comments
 (0)