Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Vectorize {Last}IndexOfAny{Except} for ASCII needles
  • Loading branch information
MihaZupan committed Nov 3, 2022
commit 69743df25c0b8425b9d6d155b28864509970e617
66 changes: 66 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,70 @@ public static void MakeSureNoChecksGoOutOfRangeMany_Char()
Assert.Equal(-1, index);
}
}

[Fact]
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
{
public delegate int IndexOfAnySearchDelegate(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> values);

public static void TestRandomInputs(IndexOfAnySearchDelegate expected, IndexOfAnySearchDelegate actual)
{
TestRandomInputs(127, expected, actual);
TestRandomInputs(char.MaxValue, expected, actual);
}

private static void TestRandomInputs(int maxNeedleValue, IndexOfAnySearchDelegate expected, IndexOfAnySearchDelegate actual)
{
maxNeedleValue++;

Span<char> needleSpace = stackalloc char[8];
Span<char> haystackSpace = stackalloc char[40];
var rng = new Random();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this reproducable, we should include a seed here.

Copy link
Member Author

@MihaZupan MihaZupan Nov 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly, I removed it because the RNG part represents a significant portion of the test execution time, with the non-explicit-seed ctor using the faster impl AFAIK.

For reproducibility, I made sure to emit the exact inputs that failed as part of the error message. Does that address your concerns here?

Copy link
Member

@stephentoub stephentoub Nov 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does that address your concerns here?

Only partially. There are two concerns:

  1. That a test could fail and you not know why or be able to trigger it again.
  2. That two runs of the same test suite might be non-deterministic.

Outputting the exact inputs addresses (1) but not (2). And (2) in general contributes to test flakiness.

If this test is about randomly stressing the implementation, then I don't believe it belongs in the functional test suite. If it's about using pseudo-randomness to try out thousands of inputs without having to manually code them all and be creative enough to ensure a reasonable spread of inputs, then it should be done in a way that runs the same tests every time, which means using a seed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed it to waste less time in Random and use a constant seed.

It was extremely useful when working on the initial implementation to get rid of all the edge-case bugs. Some were so odd that I don't believe we would have come up with test cases for otherwise.

I increased the number of iterations so it now takes about a second of CPU time to run through them, so I moved these to outerloop.
Outerloop already takes ~4 minutes on my machine so I'm gonna assume that's acceptable.


for (int iterations = 0; iterations < 100_000; iterations++)
{
Span<char> needle = needleSpace.Slice(0, rng.Next(needleSpace.Length + 1));
Span<char> haystack = haystackSpace.Slice(0, rng.Next(haystackSpace.Length + 1));

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

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

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}'");
}
}
}
}
}
42 changes: 42 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,48 @@ public void SearchingNulls(string[] input, string[] targets, int expected)
break;
}
}

[Fact]
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]
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
21 changes: 21 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,26 @@ public static void LastIndexOfAnyNullSequence_String(string[] spanInput, string[
}
}
}

[Fact]
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 @@ -414,6 +414,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