Skip to content

Commit

Permalink
Vectorize {Last}IndexOfAny{Except} for ASCII needles (#76740)
Browse files Browse the repository at this point in the history
* Vectorize {Last}IndexOfAny{Except} for ASCII needles

* Add a ShouldUseSimpleLoop helper

* Review feedback
  • Loading branch information
MihaZupan authored Nov 4, 2022
1 parent 6c5a440 commit 6dfd63c
Show file tree
Hide file tree
Showing 8 changed files with 671 additions and 77 deletions.
82 changes: 82 additions & 0 deletions src/libraries/System.Memory/tests/Span/IndexOfAny.char.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using Xunit;

Expand Down Expand Up @@ -772,5 +773,86 @@ public static void MakeSureNoChecksGoOutOfRangeMany_Char()
Assert.Equal(-1, index);
}
}

[Fact]
[OuterLoop("Takes about a second to execute")]
public static void TestIndexOfAny_RandomInputs_Char()
{
IndexOfAnyCharTestHelper.TestRandomInputs(
expected: IndexOfAnyReferenceImpl,
actual: (searchSpace, values) => searchSpace.IndexOfAny(values));

static int IndexOfAnyReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
{
for (int i = 0; i < searchSpace.Length; i++)
{
if (values.Contains(searchSpace[i]))
{
return i;
}
}

return -1;
}
}
}

public static class IndexOfAnyCharTestHelper
{
private static readonly char[] s_randomAsciiChars;
private static readonly char[] s_randomChars;

static IndexOfAnyCharTestHelper()
{
s_randomAsciiChars = new char[10 * 1024];
s_randomChars = new char[1024 * 1024];

var rng = new Random(42);

for (int i = 0; i < s_randomAsciiChars.Length; i++)
{
s_randomAsciiChars[i] = (char)rng.Next(0, 128);
}

rng.NextBytes(MemoryMarshal.Cast<char, byte>(s_randomChars));
}

public delegate int IndexOfAnySearchDelegate(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values);

public static void TestRandomInputs(IndexOfAnySearchDelegate expected, IndexOfAnySearchDelegate actual)
{
var rng = new Random(42);

for (int iterations = 0; iterations < 1_000_000; iterations++)
{
// There are more interesting corner cases with ASCII needles, stress those more.
Test(s_randomChars, s_randomAsciiChars);

Test(s_randomChars, s_randomChars);
}

void Test(ReadOnlySpan<char> haystackRandom, ReadOnlySpan<char> needleRandom)
{
const int MaxNeedleLength = 8;
const int MaxHaystackLength = 40;

ReadOnlySpan<char> haystack = haystackRandom.Slice(rng.Next(haystackRandom.Length + 1));
haystack = haystack.Slice(0, Math.Min(haystack.Length, rng.Next(MaxHaystackLength)));

ReadOnlySpan<char> needle = needleRandom.Slice(rng.Next(needleRandom.Length + 1));
needle = needle.Slice(0, Math.Min(needle.Length, rng.Next(MaxNeedleLength)));

int expectedIndex = expected(haystack, needle);
int actualIndex = actual(haystack, needle);

if (expectedIndex != actualIndex)
{
string readableNeedle = string.Join(", ", needle.ToString().Select(c => (int)c));
string readableHaystack = string.Join(", ", haystack.ToString().Select(c => (int)c));

Assert.True(false, $"Expected {expectedIndex}, got {actualIndex} for needle='{readableNeedle}', haystack='{readableHaystack}'");
}
}
}
}
}
44 changes: 44 additions & 0 deletions src/libraries/System.Memory/tests/Span/IndexOfAnyExcept.T.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,50 @@ public void SearchingNulls(string[] input, string[] targets, int expected)
break;
}
}

[Fact]
[OuterLoop("Takes about a second to execute")]
public static void TestIndexOfAnyExcept_RandomInputs_Char()
{
IndexOfAnyCharTestHelper.TestRandomInputs(
expected: IndexOfAnyExceptReferenceImpl,
actual: (searchSpace, values) => searchSpace.IndexOfAnyExcept(values));

static int IndexOfAnyExceptReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
{
for (int i = 0; i < searchSpace.Length; i++)
{
if (!values.Contains(searchSpace[i]))
{
return i;
}
}

return -1;
}
}

[Fact]
[OuterLoop("Takes about a second to execute")]
public static void TestLastIndexOfAnyExcept_RandomInputs_Char()
{
IndexOfAnyCharTestHelper.TestRandomInputs(
expected: LastIndexOfAnyExceptReferenceImpl,
actual: (searchSpace, values) => searchSpace.LastIndexOfAnyExcept(values));

static int LastIndexOfAnyExceptReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
{
for (int i = searchSpace.Length - 1; i >= 0; i--)
{
if (!values.Contains(searchSpace[i]))
{
return i;
}
}

return -1;
}
}
}

public record SimpleRecord(int Value);
Expand Down
22 changes: 22 additions & 0 deletions src/libraries/System.Memory/tests/Span/LastIndexOfAny.T.cs
Original file line number Diff line number Diff line change
Expand Up @@ -951,5 +951,27 @@ public static void LastIndexOfAnyNullSequence_String(string[] spanInput, string[
}
}
}

[Fact]
[OuterLoop("Takes about a second to execute")]
public static void TestLastIndexOfAny_RandomInputs_Char()
{
IndexOfAnyCharTestHelper.TestRandomInputs(
expected: LastIndexOfAnyReferenceImpl,
actual: (searchSpace, values) => searchSpace.LastIndexOfAny(values));

static int LastIndexOfAnyReferenceImpl(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values)
{
for (int i = searchSpace.Length - 1; i >= 0; i--)
{
if (values.Contains(searchSpace[i]))
{
return i;
}
}

return -1;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\IFormatProvider.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IFormattable.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Index.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IndexOfAnyAsciiSearcher.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\IndexOutOfRangeException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\InsufficientExecutionStackException.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\InsufficientMemoryException.cs" />
Expand Down
Loading

0 comments on commit 6dfd63c

Please sign in to comment.