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

Fix RegexOptions.Compiled|IgnoreCase perf when dynamic code isn't supported #107874

Merged
merged 1 commit into from
Sep 17, 2024

Conversation

stephentoub
Copy link
Member

If a regex is created with RegexOptions.Compiled and RegexOptions.IgnoreCase, and it begins with a pattern that's a reasonably small number of alternating strings, it'll now end up using SearchValues<string> to find the next possible match location. However, the SearchValues<string> instance doesn't end up getting created if the interpreter is being used. If the implementation falls back to the interpreter because compilation isn't supported because dynamic code isn't supported, then it won't use any optimizations to find the next starting location. That's a regression from when it would previously at least use a vectorized search to find one character class from the set of starting strings.

This fixes it to just always create the SearchValues<string>. This adds some overhead when using RegexOptions.Compiled, but it's typically just a few percentage points, and only applies in the cases where this SearchValues<string> optimization kicks in. At the moment, changing it to have perfect knowledge about whether it can avoid that creation is too invasive. This overhead also doesn't apply to the source generator.

Contributes to #99553 (this should be backported to release/9.0)

…ported

If a regex is created with RegexOptions.Compiled and RegexOptions.IgnoreCase, and it begins with a pattern that's a reasonably small number of alternating strings, it'll now end up using `SearchValues<string>` to find the next possible match location. However, the `SearchValues<string>` instance doesn't end up getting created if the interpreter is being used. If the implementation falls back to the interpreter because compilation isn't supported because dynamic code isn't supported, then it won't use any optimizations to find the next starting location. That's a regression from when it would previously at least use a vectorized search to find one character class from the set of starting strings.

This fixes it to just always create the `SearchValues<string>`. This adds some overhead when using RegexOptions.Compiled, but it's typically just a few percentage points, and only applies in the cases where this `SearchValues<string>` optimization kicks in. At the moment, changing it to have perfect knowledge about whether it can avoid that creation is too invasive. This overhead also doesn't apply to the source generator.
Copy link
Contributor

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

Copy link
Member

@MihaZupan MihaZupan left a comment

Choose a reason for hiding this comment

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

LGTM. Thank you for the clear description.

@stephentoub
Copy link
Member Author

@BrzVlad, this makes the regression go away for the cited case of IsDynamicCodeSupported being false with coreclr (rather than regressing, .NET 8 to .NET 9 improves by almost ~40% for me on the offending benchmark). Do you similarly see the same for mono? I want to make sure there aren't two issues lurking.

@stephentoub
Copy link
Member Author

/backport to release/9.0

Copy link
Contributor

Started backporting to release/9.0: https://github.com/dotnet/runtime/actions/runs/10888162147

@BrzVlad
Copy link
Member

BrzVlad commented Sep 16, 2024

@stephentoub Thanks for the quick fix. The huge regression I was observing indeed goes away. There seems to be a regression of only 1.75x compared to .NET8 on mono interpreter. 85% of the time is now spent running the method System.Buffers.AhoCorasick:IndexOfAnyCore<System.Buffers.StringSearchValuesHelper/CaseInsensitiveAsciiLetters, System.Buffers.AhoCorasick/NoFastScan> (System.ReadOnlySpan`1<char>). I think this is now consistent with your theory that SearchValues is slow on mono interpreter. I assume that, in your tests, CoreCLR was hitting the vectorized path, while the non-vectorized path didn't get much love and is now slower on interpreter (interpreter has vectorized V128 but doesn't yet implement sse, advsimd etc apis). I don't have a strong opinion on how relevant this smaller regression is, but if some quick fixes can be done to the non-vecorized path then it would be great.

@stephentoub
Copy link
Member Author

but if some quick fixes can be done to the non-vecorized path then it would be great.

I'd defer to @MihaZupan for that, but I suspect it's unlikely.

@MihaZupan
Copy link
Member

@BrzVlad would you be able to test the performance with this patch applied as well?
https://github.com/dotnet/runtime/compare/main...MihaZupan:runtime:searchvalues-string-ahoNonVecAscii?w=1

If that doesn't help much, I think the next best thing would be reviving #92680, but that seems like a .NET 10 change to me.

@BrzVlad
Copy link
Member

BrzVlad commented Sep 17, 2024

@MihaZupan That patch reduced the regression from 1.75x to 1.6x compared to .NET8. I think it is fine to not attempt to fix this for .NET9

@lewing
Copy link
Member

lewing commented Sep 17, 2024

/ba-g the stuck leg is hitting dotnet/dnceng#3879 all failures are known

@lewing lewing merged commit 8ae3796 into dotnet:main Sep 17, 2024
80 of 85 checks passed
jtschuster pushed a commit to jtschuster/runtime that referenced this pull request Sep 17, 2024
…ported (dotnet#107874)

If a regex is created with RegexOptions.Compiled and RegexOptions.IgnoreCase, and it begins with a pattern that's a reasonably small number of alternating strings, it'll now end up using `SearchValues<string>` to find the next possible match location. However, the `SearchValues<string>` instance doesn't end up getting created if the interpreter is being used. If the implementation falls back to the interpreter because compilation isn't supported because dynamic code isn't supported, then it won't use any optimizations to find the next starting location. That's a regression from when it would previously at least use a vectorized search to find one character class from the set of starting strings.

This fixes it to just always create the `SearchValues<string>`. This adds some overhead when using RegexOptions.Compiled, but it's typically just a few percentage points, and only applies in the cases where this `SearchValues<string>` optimization kicks in. At the moment, changing it to have perfect knowledge about whether it can avoid that creation is too invasive. This overhead also doesn't apply to the source generator.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants