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

Commit 9e078ff

Browse files
authored
Vectorized SequenceCompareTo for Span<char> (#17237)
- This change makes the compare for very short Span strings a bit slower and for longer Span strings many times faster. - Switch several places where it was a clear benefit to use it. `String.CompareOrdinal(string,string)` is notable exception that I have left intact for now. It is fine tuned for current string layout, and replacing with a call of vectorized SequenceCompareTo gives mixed results.
1 parent 6b3d2dd commit 9e078ff

File tree

11 files changed

+253
-407
lines changed

11 files changed

+253
-407
lines changed

src/classlibnative/bcltype/stringnative.cpp

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -109,44 +109,6 @@ FCIMPL2(INT32, COMString::FCCompareOrdinalIgnoreCaseWC, StringObject* strA, __in
109109
}
110110
FCIMPLEND
111111

112-
/*================================CompareOrdinalEx===============================
113-
**Args: typedef struct {STRINGREF thisRef; INT32 options; INT32 length; INT32 valueOffset;\
114-
STRINGREF value; INT32 thisOffset;} _compareOrdinalArgsEx;
115-
==============================================================================*/
116-
117-
FCIMPL6(INT32, COMString::CompareOrdinalEx, StringObject* strA, INT32 indexA, INT32 countA, StringObject* strB, INT32 indexB, INT32 countB)
118-
{
119-
FCALL_CONTRACT;
120-
121-
VALIDATEOBJECT(strA);
122-
VALIDATEOBJECT(strB);
123-
DWORD *strAChars, *strBChars;
124-
int strALength, strBLength;
125-
126-
// These runtime tests are handled in the managed wrapper.
127-
_ASSERTE(strA != NULL && strB != NULL);
128-
_ASSERTE(indexA >= 0 && indexB >= 0);
129-
_ASSERTE(countA >= 0 && countB >= 0);
130-
131-
strA->RefInterpretGetStringValuesDangerousForGC((WCHAR **) &strAChars, &strALength);
132-
strB->RefInterpretGetStringValuesDangerousForGC((WCHAR **) &strBChars, &strBLength);
133-
134-
_ASSERTE(countA <= strALength - indexA);
135-
_ASSERTE(countB <= strBLength - indexB);
136-
137-
// Set up the loop variables.
138-
strAChars = (DWORD *) ((WCHAR *) strAChars + indexA);
139-
strBChars = (DWORD *) ((WCHAR *) strBChars + indexB);
140-
141-
INT32 result = StringObject::FastCompareStringHelper(strAChars, countA, strBChars, countB);
142-
143-
FC_GC_POLL_RET();
144-
return result;
145-
146-
}
147-
FCIMPLEND
148-
149-
150112
/*==================================GETCHARAT===================================
151113
**Returns the character at position index. Thows IndexOutOfRangeException as
152114
**appropriate.

src/mscorlib/shared/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@
492492
<Compile Include="$(MSBuildThisFileDirectory)System\SpanHelpers.cs" />
493493
<Compile Include="$(MSBuildThisFileDirectory)System\SpanHelpers.BinarySearch.cs" />
494494
<Compile Include="$(MSBuildThisFileDirectory)System\SpanHelpers.Byte.cs" />
495+
<Compile Include="$(MSBuildThisFileDirectory)System\SpanHelpers.Char.cs" />
495496
<Compile Include="$(MSBuildThisFileDirectory)System\SpanHelpers.T.cs" />
496497
<Compile Include="$(MSBuildThisFileDirectory)System\String.cs" />
497498
<Compile Include="$(MSBuildThisFileDirectory)System\String.Manipulation.cs" />

src/mscorlib/shared/System/Globalization/CompareInfo.cs

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ public virtual int Compare(string string1, string string2, CompareOptions option
341341
if (_invariantMode)
342342
{
343343
if ((options & CompareOptions.IgnoreCase) != 0)
344-
return CompareOrdinalIgnoreCase(string1, 0, string1.Length, string2, 0, string2.Length);
344+
return CompareOrdinalIgnoreCase(string1, string2);
345345

346346
return String.CompareOrdinal(string1, string2);
347347
}
@@ -501,35 +501,23 @@ public virtual int Compare(string string1, int offset1, int length1, string stri
501501
return (1);
502502
}
503503

504+
ReadOnlySpan<char> span1 = string1.AsSpan(offset1, length1);
505+
ReadOnlySpan<char> span2 = string2.AsSpan(offset2, length2);
506+
504507
if (options == CompareOptions.Ordinal)
505508
{
506-
return CompareOrdinal(string1, offset1, length1,
507-
string2, offset2, length2);
509+
return string.CompareOrdinal(span1, span2);
508510
}
509511

510512
if (_invariantMode)
511513
{
512514
if ((options & CompareOptions.IgnoreCase) != 0)
513-
return CompareOrdinalIgnoreCase(string1, offset1, length1, string2, offset2, length2);
515+
return CompareOrdinalIgnoreCase(span1, span2);
514516

515-
return CompareOrdinal(string1, offset1, length1, string2, offset2, length2);
517+
return string.CompareOrdinal(span1, span2);
516518
}
517519

518-
return CompareString(
519-
string1.AsSpan(offset1, length1),
520-
string2.AsSpan(offset2, length2),
521-
options);
522-
}
523-
524-
private static int CompareOrdinal(string string1, int offset1, int length1, string string2, int offset2, int length2)
525-
{
526-
int result = String.CompareOrdinal(string1, offset1, string2, offset2,
527-
(length1 < length2 ? length1 : length2));
528-
if ((length1 != length2) && result == 0)
529-
{
530-
return (length1 > length2 ? 1 : -1);
531-
}
532-
return (result);
520+
return CompareString(span1, span2, options);
533521
}
534522

535523
//

src/mscorlib/shared/System/MemoryExtensions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,13 @@ ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(span)),
301301
ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(other)),
302302
other.Length);
303303

304+
if (typeof(T) == typeof(char))
305+
return SpanHelpers.SequenceCompareTo(
306+
ref Unsafe.As<T, char>(ref MemoryMarshal.GetReference(span)),
307+
span.Length,
308+
ref Unsafe.As<T, char>(ref MemoryMarshal.GetReference(other)),
309+
other.Length);
310+
304311
return SpanHelpers.SequenceCompareTo(ref MemoryMarshal.GetReference(span), span.Length, ref MemoryMarshal.GetReference(other), other.Length);
305312
}
306313

@@ -659,6 +666,13 @@ ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(span)),
659666
ref Unsafe.As<T, byte>(ref MemoryMarshal.GetReference(other)),
660667
other.Length);
661668

669+
if (typeof(T) == typeof(char))
670+
return SpanHelpers.SequenceCompareTo(
671+
ref Unsafe.As<T, char>(ref MemoryMarshal.GetReference(span)),
672+
span.Length,
673+
ref Unsafe.As<T, char>(ref MemoryMarshal.GetReference(other)),
674+
other.Length);
675+
662676
return SpanHelpers.SequenceCompareTo(ref MemoryMarshal.GetReference(span), span.Length, ref MemoryMarshal.GetReference(other), other.Length);
663677
}
664678

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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 System.Diagnostics;
6+
using System.Runtime.CompilerServices;
7+
8+
#if !netstandard
9+
using Internal.Runtime.CompilerServices;
10+
#endif
11+
12+
#if !netstandard11
13+
using System.Numerics;
14+
#endif
15+
16+
namespace System
17+
{
18+
internal static partial class SpanHelpers
19+
{
20+
public static unsafe int SequenceCompareTo(ref char first, int firstLength, ref char second, int secondLength)
21+
{
22+
Debug.Assert(firstLength >= 0);
23+
Debug.Assert(secondLength >= 0);
24+
25+
int lengthDelta = firstLength - secondLength;
26+
27+
if (Unsafe.AreSame(ref first, ref second))
28+
goto Equal;
29+
30+
IntPtr minLength = (IntPtr)((firstLength < secondLength) ? firstLength : secondLength);
31+
IntPtr i = (IntPtr)0; // Use IntPtr for arithmetic to avoid unnecessary 64->32->64 truncations
32+
33+
if ((byte*)minLength >= (byte*)(sizeof(UIntPtr) / sizeof(char)))
34+
{
35+
#if !netstandard11
36+
if (Vector.IsHardwareAccelerated && (byte*)minLength >= (byte*)Vector<ushort>.Count)
37+
{
38+
IntPtr nLength = minLength - Vector<ushort>.Count;
39+
do
40+
{
41+
if (Unsafe.ReadUnaligned<Vector<ushort>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref first, i))) !=
42+
Unsafe.ReadUnaligned<Vector<ushort>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref second, i))))
43+
{
44+
break;
45+
}
46+
i += Vector<ushort>.Count;
47+
}
48+
while ((byte*)nLength >= (byte*)i);
49+
}
50+
#endif
51+
52+
while ((byte*)minLength >= (byte*)(i + sizeof(UIntPtr) / sizeof(char)))
53+
{
54+
if (Unsafe.ReadUnaligned<UIntPtr>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref first, i))) !=
55+
Unsafe.ReadUnaligned<UIntPtr>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref second, i))))
56+
{
57+
break;
58+
}
59+
i += sizeof(UIntPtr) / sizeof(char);
60+
}
61+
}
62+
63+
if (sizeof(UIntPtr) > sizeof(int) && (byte*)minLength >= (byte*)(i + sizeof(int) / sizeof(char)))
64+
{
65+
if (Unsafe.ReadUnaligned<int>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref first, i))) ==
66+
Unsafe.ReadUnaligned<int>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref second, i))))
67+
{
68+
i += sizeof(int) / sizeof(char);
69+
}
70+
}
71+
72+
while ((byte*)i < (byte*)minLength)
73+
{
74+
int result = Unsafe.Add(ref first, i).CompareTo(Unsafe.Add(ref second, i));
75+
if (result != 0) return result;
76+
i += 1;
77+
}
78+
79+
Equal:
80+
return lengthDelta;
81+
}
82+
83+
public static unsafe int IndexOf(ref char searchSpace, char value, int length)
84+
{
85+
Debug.Assert(length >= 0);
86+
87+
uint uValue = value; // Use uint for comparisons to avoid unnecessary 8->32 extensions
88+
IntPtr index = (IntPtr)0; // Use IntPtr for arithmetic to avoid unnecessary 64->32->64 truncations
89+
IntPtr nLength = (IntPtr)length;
90+
#if !netstandard11
91+
if (Vector.IsHardwareAccelerated && length >= Vector<ushort>.Count * 2)
92+
{
93+
const int elementsPerByte = sizeof(ushort) / sizeof(byte);
94+
int unaligned = ((int)Unsafe.AsPointer(ref searchSpace) & (Vector<byte>.Count - 1)) / elementsPerByte;
95+
nLength = (IntPtr)((Vector<ushort>.Count - unaligned) & (Vector<ushort>.Count - 1));
96+
}
97+
SequentialScan:
98+
#endif
99+
while ((byte*)nLength >= (byte*)4)
100+
{
101+
nLength -= 4;
102+
103+
if (uValue == Unsafe.Add(ref searchSpace, index))
104+
goto Found;
105+
if (uValue == Unsafe.Add(ref searchSpace, index + 1))
106+
goto Found1;
107+
if (uValue == Unsafe.Add(ref searchSpace, index + 2))
108+
goto Found2;
109+
if (uValue == Unsafe.Add(ref searchSpace, index + 3))
110+
goto Found3;
111+
112+
index += 4;
113+
}
114+
115+
while ((byte*)nLength > (byte*)0)
116+
{
117+
nLength -= 1;
118+
119+
if (uValue == Unsafe.Add(ref searchSpace, index))
120+
goto Found;
121+
122+
index += 1;
123+
}
124+
#if !netstandard11
125+
if (Vector.IsHardwareAccelerated && ((int)(byte*)index < length))
126+
{
127+
nLength = (IntPtr)((length - (int)(byte*)index) & ~(Vector<ushort>.Count - 1));
128+
129+
// Get comparison Vector
130+
Vector<ushort> vComparison = new Vector<ushort>(value);
131+
132+
while ((byte*)nLength > (byte*)index)
133+
{
134+
var vMatches = Vector.Equals(vComparison, Unsafe.ReadUnaligned<Vector<ushort>>(ref Unsafe.As<char, byte>(ref Unsafe.Add(ref searchSpace, index))));
135+
if (Vector<ushort>.Zero.Equals(vMatches))
136+
{
137+
index += Vector<ushort>.Count;
138+
continue;
139+
}
140+
// Find offset of first match
141+
return (int)(byte*)index + LocateFirstFoundChar(vMatches);
142+
}
143+
144+
if ((int)(byte*)index < length)
145+
{
146+
nLength = (IntPtr)(length - (int)(byte*)index);
147+
goto SequentialScan;
148+
}
149+
}
150+
#endif
151+
return -1;
152+
Found: // Workaround for https://github.com/dotnet/coreclr/issues/13549
153+
return (int)(byte*)index;
154+
Found1:
155+
return (int)(byte*)(index + 1);
156+
Found2:
157+
return (int)(byte*)(index + 2);
158+
Found3:
159+
return (int)(byte*)(index + 3);
160+
}
161+
162+
#if !netstandard11
163+
// Vector sub-search adapted from https://github.com/aspnet/KestrelHttpServer/pull/1138
164+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
165+
private static int LocateFirstFoundChar(Vector<ushort> match)
166+
{
167+
var vector64 = Vector.AsVectorUInt64(match);
168+
ulong candidate = 0;
169+
int i = 0;
170+
// Pattern unrolled by jit https://github.com/dotnet/coreclr/pull/8001
171+
for (; i < Vector<ulong>.Count; i++)
172+
{
173+
candidate = vector64[i];
174+
if (candidate != 0)
175+
{
176+
break;
177+
}
178+
}
179+
180+
// Single LEA instruction with jitted const (using function result)
181+
return i * 4 + LocateFirstFoundChar(candidate);
182+
}
183+
184+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
185+
private static int LocateFirstFoundChar(ulong match)
186+
{
187+
unchecked
188+
{
189+
// Flag least significant power of two bit
190+
var powerOfTwoFlag = match ^ (match - 1);
191+
// Shift all powers of two into the high byte and extract
192+
return (int)((powerOfTwoFlag * XorPowerOfTwoToHighChar) >> 49);
193+
}
194+
}
195+
196+
private const ulong XorPowerOfTwoToHighChar = (0x03ul |
197+
0x02ul << 16 |
198+
0x01ul << 32) + 1;
199+
#endif
200+
}
201+
}

0 commit comments

Comments
 (0)