Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve vectorization of IndexOf(chars, StringComparison.OrdinalIgnoreCase) #85437

Merged
merged 3 commits into from
May 1, 2023

Conversation

stephentoub
Copy link
Member

@stephentoub stephentoub commented Apr 27, 2023

Use the same general "Algorithm 1: Generic SIMD" that we do for StringComparison.Ordinal, adapted for OrdinalIgnoreCase.

private static readonly string s_haystack = new HttpClient().GetStringAsync("https://www.gutenberg.org/files/1661/1661-0.txt").Result;

[Params("watson", "elementary", "holmes", "the")]
public string Needle { get; set; }

[Benchmark]
public int Count()
{
    int count = 0;
    ReadOnlySpan<char> haystack = s_haystack;
    while (true)
    {
        int pos = haystack.IndexOf(Needle, StringComparison.OrdinalIgnoreCase);
        if (pos < 0) break;
        count++;
        haystack = haystack.Slice(pos + Needle.Length);
    }
    return count;
}
Method Toolchain Needle Mean Error StdDev Ratio
Count \main\corerun.exe elementary 580.54 us 3.562 us 2.781 us 1.00
Count \pr\corerun.exe elementary 59.37 us 0.447 us 0.397 us 0.10
Count \main\corerun.exe holmes 366.58 us 0.607 us 0.568 us 1.00
Count \pr\corerun.exe holmes 82.55 us 0.204 us 0.181 us 0.23
Count \main\corerun.exe the 547.91 us 1.257 us 1.050 us 1.00
Count \pr\corerun.exe the 258.62 us 1.123 us 0.996 us 0.47
Count \main\corerun.exe watson 230.76 us 0.550 us 0.514 us 1.00
Count \pr\corerun.exe watson 58.24 us 0.448 us 0.419 us 0.25

@stephentoub stephentoub added this to the 8.0.0 milestone Apr 27, 2023
@ghost ghost assigned stephentoub Apr 27, 2023
@ghost
Copy link

ghost commented Apr 27, 2023

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

Use the same general "Algorithm 1: Generic SIMD" that we do for StringComparison.Ordinal, adapter for OrdinalIgnoreCase.

[Params("watson", "elementary", "holmes", "the")]
public string Needle { get; set; }

[Benchmark]
public int Count()
{
    int count = 0;
    ReadOnlySpan<char> haystack = s_haystack;
    while (true)
    {
        int pos = haystack.IndexOf(Needle, StringComparison.OrdinalIgnoreCase);
        if (pos < 0) break;
        count++;
        haystack = haystack.Slice(pos + Needle.Length);
    }
    return count;
}
Method Toolchain Needle Mean Error StdDev Ratio
Count \main\corerun.exe elementary 580.54 us 3.562 us 2.781 us 1.00
Count \pr\corerun.exe elementary 59.37 us 0.447 us 0.397 us 0.10
Count \main\corerun.exe holmes 366.58 us 0.607 us 0.568 us 1.00
Count \pr\corerun.exe holmes 82.55 us 0.204 us 0.181 us 0.23
Count \main\corerun.exe the 547.91 us 1.257 us 1.050 us 1.00
Count \pr\corerun.exe the 258.62 us 1.123 us 0.996 us 0.47
Count \main\corerun.exe watson 230.76 us 0.550 us 0.514 us 1.00
Count \pr\corerun.exe watson 58.24 us 0.448 us 0.419 us 0.25
Author: stephentoub
Assignees: -
Labels:

area-System.Runtime, tenet-performance

Milestone: 8.0.0

…eCase)

Use the same general "Algorithm 1: Generic SIMD" that we do for StringComparison.Ordinal, adapter for OrdinalIgnoreCase.
// Load a vector from the current search space offset and another from the offset plus the distance between the two characters.
// For each, | with 0x20 so that letters are lowercased, then & those together to get a mask. If the mask is all zeros, there
// was no match. If it wasn't, we have to do more work to check for a match.
Vector128<ushort> cmpCh2 = Vector128.Equals(ch2, Vector128.BitwiseOr(Vector128.LoadUnsafe(ref searchSpace, (nuint)(offset + ch1ch2Distance)), Vector128.Create((ushort)0x20)));
Copy link
Member

Choose a reason for hiding this comment

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

Very nit: Vector128.BitwiseOr -> |

Copy link
Member Author

Choose a reason for hiding this comment

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

This is just style, right? Happy to change it, just questioning whether it's worth rerunning ci.

Copy link
Member

Choose a reason for hiding this comment

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

definitely not worth it 🙂

Copy link
Member

Choose a reason for hiding this comment

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

Right its "just style". There is also the general considerations of "methods" vs "operators" (such as precedence and readability) but we're not super consistent today just due to the operators being relatively new.

Copy link
Member

@EgorBo EgorBo left a comment

Choose a reason for hiding this comment

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

Nice! Assuming we're fine with the overhead for the worst case - it's slightly bigger in case of OrdinalIgnoreCase due to more work + the path to find unique chars is more expensive, but the benefits should outweight that 👍

@stephentoub
Copy link
Member Author

I think it's worth it. It's more expensive to set up than ordinal, but the match validation that happens on every potential match is also more expensive, and this generally lessens the latter. It will regress in cases similar to ordinal regressed, eg where the starting character never matches, but on the balance I expect it'll be a meaningful win. Let's try and see what falls out. :-)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants