-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Improve RegexCompiler's ToLower perf #35203
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
Conversation
Currently GenerateGo/FindFirstChar create a few _tempLocal1, _tempLocal2, etc. LocalBuilders that various parts of the methods may use. The developer then need to keep track of which of these locals is currently being used such that we maximize the re-use of them while not accidentally overlapping their use. And the names end up being non-descriptive, so in some cases additional names are added for convenience. The primary purpose of the PR is cleanup. It adds a pool of LocalBuilders that lets them be rented just where they're needed, such that we only create locals when necessary, can give them appropriate names, etc. However, it also makes it a lot easier for helpers to create temporary locals (e.g. as was done in EmitMatchCharacterClass) which then makes it easier to add additional optimizations.
To support RegexOptions.IgnoreCase, RegexCompiler ends up generating a call to `char.ToLower(c, _culture)` for every character compared. That `ToLower` call in turn needs to access `_culture.TextInfo`, and then call its `ToLower(char)` virtual method. We can instead get the `TextInfo` once and call its `ToLower` method directly, saving a virtual call on every character being compared. Note that we were already doing this (for .NET 5) in RegexInterpreter, but hadn't done it in RegexCompiler because of complications around locals management. The previous commit fixed that and made this possible.
|
Tagging subscribers to this area: @eerhardt |
...libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs
Outdated
Show resolved
Hide resolved
...libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs
Outdated
Show resolved
Hide resolved
…egularExpressions/RegexCompiler.cs
|
@stephentoub We recently changed (Edit: I suppose you might be relying on the implicit null check that |
eerhardt
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks super clean. I like how this turned out vs the 3 _tempXLocal variables.
| _int32LocalsPool ??= new Stack<LocalBuilder>(), | ||
| _int32LocalsPool.TryPop(out LocalBuilder? iterationLocal) ? iterationLocal : DeclareInt32()); | ||
|
|
||
| /// <summary>Rents an ReadOnlySpan(char) local variable slot from the pool of locals.</summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(super nit) Rents an ReadOnlySpan => Rents a ReadOnlySpan
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. I'll fix the comment in the other PR, to avoid restarting CI.
It'll still invoke it non-virtually, and potentially even inline it; it'll just include the null check if needed. You think it's worthwhile changing it to be call instead? Last I knew, in general the C# compiler emits callvirt even for non-virtual methods, except in some circumstances, such as with the ?. operator. If we want to change it, I think we should change other uses of callvirt as well, e.g. various string methods are currently invoked with callvirt instead of call: Line 528 in 774daf4
|
Profiling shows that
RegexOptions.Compiled | RegexOptions.IgnoreCaseregexes are spending a non-trivial amount of time invokingchar.ToLower(c, _culture), and a good chunk of this is just in making the virtual call to_culture.TextInfoinside ofchar.ToLower. Instead of caching aCultureInfoand callingchar.ToLower(c, _culture),RegexCompilercan cache theTextInfoonce and then call_textInfo.ToLower(c), saving a virtual call on every character comparison.That's the second commit. The first commit addresses why this change hasn't been done until now. We'd previously made the same change in
RegexInterpreter, but inRegexCompiler, we need a temporary in order to swap parameters to make theToLowerlower call (by the timeCallToLoweris invoked, thecharis already on the evaluation stack). So, the first commit fixes howRegexCompilermanages temporary locals. Previously, the developer would declare a few temporary locals at the beginning of the method, and then manually figure out how to use them in a safe manner. With this change, we instead maintain a pool of locals that can be rented from just where they're needed. That makes it possible for a helper likeCallToLowerto just grab a local for the brief period it needs one.This PR handles the
RegexOptions.IgnoreCasescenario. #35194 covers theRegexOptions.IgnoreCase | RegexOptions.InvariantCulturescenario, as thereRegexCompilerusesToLowerInvariant.Review without whitespace diffs: https://github.com/dotnet/runtime/pull/35203/files?w=1
cc: @GrabYourPitchforks, @eerhardt, @pgovind