Skip to content

Commit 6dfd63c

Browse files
authored
Vectorize {Last}IndexOfAny{Except} for ASCII needles (#76740)
* Vectorize {Last}IndexOfAny{Except} for ASCII needles * Add a ShouldUseSimpleLoop helper * Review feedback
1 parent 6c5a440 commit 6dfd63c

8 files changed

Lines changed: 671 additions & 77 deletions

File tree

src/libraries/System.Memory/tests/Span/IndexOfAny.char.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Linq;
55
using System.Numerics;
6+
using System.Runtime.InteropServices;
67
using System.Text;
78
using Xunit;
89

@@ -772,5 +773,86 @@ public static void MakeSureNoChecksGoOutOfRangeMany_Char()
772773
Assert.Equal(-1, index);
773774
}
774775
}
776+
777+
[Fact]
778+
[OuterLoop("Takes about a second to execute")]
779+
public static void TestIndexOfAny_RandomInputs_Char()
780+
{
781+
IndexOfAnyCharTestHelper.TestRandomInputs(
782+
expected: IndexOfAnyReferenceImpl,
783+
actual: (searchSpace, values) => searchSpace.IndexOfAny(values));
784+
785+
static int IndexOfAnyReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
786+
{
787+
for (int i = 0; i < searchSpace.Length; i++)
788+
{
789+
if (values.Contains(searchSpace[i]))
790+
{
791+
return i;
792+
}
793+
}
794+
795+
return -1;
796+
}
797+
}
798+
}
799+
800+
public static class IndexOfAnyCharTestHelper
801+
{
802+
private static readonly char[] s_randomAsciiChars;
803+
private static readonly char[] s_randomChars;
804+
805+
static IndexOfAnyCharTestHelper()
806+
{
807+
s_randomAsciiChars = new char[10 * 1024];
808+
s_randomChars = new char[1024 * 1024];
809+
810+
var rng = new Random(42);
811+
812+
for (int i = 0; i < s_randomAsciiChars.Length; i++)
813+
{
814+
s_randomAsciiChars[i] = (char)rng.Next(0, 128);
815+
}
816+
817+
rng.NextBytes(MemoryMarshal.Cast<char, byte>(s_randomChars));
818+
}
819+
820+
public delegate int IndexOfAnySearchDelegate(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values);
821+
822+
public static void TestRandomInputs(IndexOfAnySearchDelegate expected, IndexOfAnySearchDelegate actual)
823+
{
824+
var rng = new Random(42);
825+
826+
for (int iterations = 0; iterations < 1_000_000; iterations++)
827+
{
828+
// There are more interesting corner cases with ASCII needles, stress those more.
829+
Test(s_randomChars, s_randomAsciiChars);
830+
831+
Test(s_randomChars, s_randomChars);
832+
}
833+
834+
void Test(ReadOnlySpan<char> haystackRandom, ReadOnlySpan<char> needleRandom)
835+
{
836+
const int MaxNeedleLength = 8;
837+
const int MaxHaystackLength = 40;
838+
839+
ReadOnlySpan<char> haystack = haystackRandom.Slice(rng.Next(haystackRandom.Length + 1));
840+
haystack = haystack.Slice(0, Math.Min(haystack.Length, rng.Next(MaxHaystackLength)));
841+
842+
ReadOnlySpan<char> needle = needleRandom.Slice(rng.Next(needleRandom.Length + 1));
843+
needle = needle.Slice(0, Math.Min(needle.Length, rng.Next(MaxNeedleLength)));
844+
845+
int expectedIndex = expected(haystack, needle);
846+
int actualIndex = actual(haystack, needle);
847+
848+
if (expectedIndex != actualIndex)
849+
{
850+
string readableNeedle = string.Join(", ", needle.ToString().Select(c => (int)c));
851+
string readableHaystack = string.Join(", ", haystack.ToString().Select(c => (int)c));
852+
853+
Assert.True(false, $"Expected {expectedIndex}, got {actualIndex} for needle='{readableNeedle}', haystack='{readableHaystack}'");
854+
}
855+
}
856+
}
775857
}
776858
}

src/libraries/System.Memory/tests/Span/IndexOfAnyExcept.T.cs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,50 @@ public void SearchingNulls(string[] input, string[] targets, int expected)
5353
break;
5454
}
5555
}
56+
57+
[Fact]
58+
[OuterLoop("Takes about a second to execute")]
59+
public static void TestIndexOfAnyExcept_RandomInputs_Char()
60+
{
61+
IndexOfAnyCharTestHelper.TestRandomInputs(
62+
expected: IndexOfAnyExceptReferenceImpl,
63+
actual: (searchSpace, values) => searchSpace.IndexOfAnyExcept(values));
64+
65+
static int IndexOfAnyExceptReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
66+
{
67+
for (int i = 0; i < searchSpace.Length; i++)
68+
{
69+
if (!values.Contains(searchSpace[i]))
70+
{
71+
return i;
72+
}
73+
}
74+
75+
return -1;
76+
}
77+
}
78+
79+
[Fact]
80+
[OuterLoop("Takes about a second to execute")]
81+
public static void TestLastIndexOfAnyExcept_RandomInputs_Char()
82+
{
83+
IndexOfAnyCharTestHelper.TestRandomInputs(
84+
expected: LastIndexOfAnyExceptReferenceImpl,
85+
actual: (searchSpace, values) => searchSpace.LastIndexOfAnyExcept(values));
86+
87+
static int LastIndexOfAnyExceptReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
88+
{
89+
for (int i = searchSpace.Length - 1; i >= 0; i--)
90+
{
91+
if (!values.Contains(searchSpace[i]))
92+
{
93+
return i;
94+
}
95+
}
96+
97+
return -1;
98+
}
99+
}
56100
}
57101

58102
public record SimpleRecord(int Value);

src/libraries/System.Memory/tests/Span/LastIndexOfAny.T.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -951,5 +951,27 @@ public static void LastIndexOfAnyNullSequence_String(string[] spanInput, string[
951951
}
952952
}
953953
}
954+
955+
[Fact]
956+
[OuterLoop("Takes about a second to execute")]
957+
public static void TestLastIndexOfAny_RandomInputs_Char()
958+
{
959+
IndexOfAnyCharTestHelper.TestRandomInputs(
960+
expected: LastIndexOfAnyReferenceImpl,
961+
actual: (searchSpace, values) => searchSpace.LastIndexOfAny(values));
962+
963+
static int LastIndexOfAnyReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
964+
{
965+
for (int i = searchSpace.Length - 1; i >= 0; i--)
966+
{
967+
if (values.Contains(searchSpace[i]))
968+
{
969+
return i;
970+
}
971+
}
972+
973+
return -1;
974+
}
975+
}
954976
}
955977
}

src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@
415415
<Compile Include="$(MSBuildThisFileDirectory)System\IFormatProvider.cs" />
416416
<Compile Include="$(MSBuildThisFileDirectory)System\IFormattable.cs" />
417417
<Compile Include="$(MSBuildThisFileDirectory)System\Index.cs" />
418+
<Compile Include="$(MSBuildThisFileDirectory)System\IndexOfAnyAsciiSearcher.cs" />
418419
<Compile Include="$(MSBuildThisFileDirectory)System\IndexOutOfRangeException.cs" />
419420
<Compile Include="$(MSBuildThisFileDirectory)System\InsufficientExecutionStackException.cs" />
420421
<Compile Include="$(MSBuildThisFileDirectory)System\InsufficientMemoryException.cs" />

0 commit comments

Comments
 (0)