-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Length-based switch dispatch #66081
Length-based switch dispatch #66081
Conversation
📝 Illustration of the tree of cases (
Refers to: src/Compilers/CSharp/Test/Emit2/CodeGen/CodeGenLengthBasedSwitchTests.cs:72 in 04e329a. [](commit_id = 04e329a, deletion_comment = False) |
// | ||
// // strings of length 1 don't need any further validation once we've checked one char | ||
// case 1: | ||
// switch (key[posM]) |
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.
can be just key[0]
here for this case?
You may already be using this - since the first step uses the string length, the compiler can remove the boundary check in the following steps. |
You could switch the check from a single character to a pair (i.e. ushort -> uint). This can noticeably reduce collisions in some cases. |
Hey, just a drive-by comment here from the peanut gallery. When I implemented the frozen collections, I pulled out all the stops to try and get as much performance possible for read-only dictionaries and sets. This resulted in appreciable gains overall. I think the compiler should be able to adopt nearly all the same techniques to achieve similar benefits. For example, I implemented support for partial string hashing. Since all the strings are known a priori, you know which part of the strings are different and can thus limit hashing to only the parts of the strings which are different. To make this work better, I added support for both left- and right-justified strings which deals with strings having common prefixes or suffxes cleanly. Go take a look at https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections.Immutable/src/System/Collections/Frozen/StringComparers/ComparerPicker.cs for the logic that selects how to compare strings within the frozen collections. I suspect most of the same logic would work within the compiler as well. |
@dotnet/roslyn-compiler for review. Thanks |
1 similar comment
@dotnet/roslyn-compiler for review. Thanks |
@@ -7406,4 +7406,7 @@ To remove the warning, you can use /reference instead (set the Embed Interop Typ | |||
<data name="WRN_ParamsArrayInLambdaOnly_Title" xml:space="preserve"> | |||
<value>Parameter has params modifier in lambda but not in target delegate type.</value> | |||
</data> | |||
<data name="IDS_DisableLengthBasedSwitch" xml:space="preserve"> | |||
<value>disable-length-based switch</value> |
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.
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.
Actually intended for hyphens throughout. Fixed and added a command-line test.
|
||
internal StringJumpTable(LabelSymbol label, ImmutableArray<(ConstantValue value, LabelSymbol label)> stringCaseLabels) | ||
{ | ||
Debug.Assert(stringCaseLabels.All(c => c.value.IsString) && stringCaseLabels.Length > 0); |
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.
var currentChar = caseLabel.value.StringValue[position]; | ||
if (countPerChar.TryGetValue(currentChar, out var currentCount)) | ||
{ | ||
countPerChar[currentChar]++; |
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.
foreach (var group in casesWithGivenLength.GroupBy(c => c.value.StringValue![bestCharacterPosition])) | ||
{ | ||
char character = group.Key; | ||
var label = CreateAndRegisterStringJumpTable(group.Select(c => c).ToImmutableArray(), stringJumpTables); |
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.
{ | ||
Debug.Assert(expression.ConstantValue == null); | ||
Debug.Assert((object)expression.Type != null && | ||
(expression.Type.IsValidV6SwitchGoverningType() || expression.Type.IsSpanOrReadOnlySpanChar())); | ||
Debug.Assert(switchCaseLabels.Length > 0); | ||
|
||
Debug.Assert(switchCaseLabels != null); |
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.
|
||
Debug.Assert(spanTLengthMethod != null && !spanTLengthMethod.HasUseSiteError); | ||
var spanCharLengthMethod = spanTLengthMethod.AsMember((NamedTypeSymbol)keyType); | ||
return _module.Translate(spanCharLengthMethod, null, _diagnostics.DiagnosticBag); |
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.
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 is moved code. The null
was in the original code. Fixed it now.
IL_0036: bne.un.s IL_00ad | ||
IL_0038: ldarg.0 | ||
IL_0039: ldc.i4.0 | ||
IL_003a: call "char string.this[int].get" |
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.
📝 we only need to do a character check, no need to follow up with a string check, since we're dealing with strings of length==1.
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
/azp run |
Azure Pipelines successfully started running 2 pipeline(s). |
@jcouv can it possibly be the reason of increased build times in dotnet/runtime? dotnet/runtime#82583 (that Roslyn update contained this PR) |
Closes #56374
Design
There are currently multiple strategies for emitting switch dispatches on strings:
This PR adds a third strategy, which is based on avoiding computing the hash code (relatively expensive):
Performance
The strategy was evaluated in terms of performance (typical, average and worst case, based on synthetic and realistic cases) and IL size. The plan is to do further performance evaluation on different runtime environments using the runtime's perf lab. The PR also includes an opt-out flag (that we could remove after some period of time) in case the optimization somehow caused a performance regression.
When the final string dispatch involves more than 7 cases, we expect the new approach to perform worse. In simplified terms, the cost of "Length dispatch + char dispatch + hashcode dispatch" is worse than that of "hashcode dispatch".
But what about "Length dispatch + character dispatch + N string comparisons"?
Based on measurements, this new approach is most clearly beneficial when the number of cases for final string dispatch (N) is 5 or lower.
So the new approach is used when there are more than 7 cases and the final string dispatch buckets have 5 or fewer cases.
Notes:
Implementation
Most of the optimization is implemented in the same place where the hashcode optimization was implemented, ie in the emit layer for
BoundSwitchDispatch
. But since the decision to emit private helpers is made in lowering (LowerSwitchDispatchNode
), the analysis of the Length+char dispatch is also performed during lowering (and the result stashed until needed for emitting).I've opted for using LINQ in a few places to favor readability. I'm open to discussing some more efficient ways of doing the bucket analysis.
I'm also open to removing some of the IL verification, to make the tests more compact.