Tuple equality: Optimize cases where nullable always has value#25644
Tuple equality: Optimize cases where nullable always has value#25644jcouv merged 7 commits intodotnet:dev15.7.xfrom
Conversation
|
@AlekseyTs @dotnet/roslyn-compiler for review. This is a follow-up optimization raised during the main code review for tuple equality (#23356 (comment), #23356 (review)) |
| // in `(..., expr) == (..., (..., ...))` we need to save `expr` because it is used in a simple comparison | ||
| return EvaluateSideEffectingArgumentToTemp(VisitExpression(expr), initEffects, ref temps); | ||
| BoundExpression loweredExpr = VisitExpression(expr); | ||
| if ((bool)(loweredExpr.Type != null) && NullableAlwaysHasValue((BoundExpression)loweredExpr) is var value && value != null) |
There was a problem hiding this comment.
(bool) [](start = 16, length = 6)
Not sure why do we need the cast to bool here #Closed
| // in `(..., expr) == (..., (..., ...))` we need to save `expr` because it is used in a simple comparison | ||
| return EvaluateSideEffectingArgumentToTemp(VisitExpression(expr), initEffects, ref temps); | ||
| BoundExpression loweredExpr = VisitExpression(expr); | ||
| if ((bool)(loweredExpr.Type != null) && NullableAlwaysHasValue((BoundExpression)loweredExpr) is var value && value != null) |
There was a problem hiding this comment.
loweredExpr.Type [](start = 23, length = 16)
Cast to object? #Closed
| // in `(..., expr) == (..., (..., ...))` we need to save `expr` because it is used in a simple comparison | ||
| return EvaluateSideEffectingArgumentToTemp(VisitExpression(expr), initEffects, ref temps); | ||
| BoundExpression loweredExpr = VisitExpression(expr); | ||
| if ((bool)(loweredExpr.Type != null) && NullableAlwaysHasValue((BoundExpression)loweredExpr) is var value && value != null) |
There was a problem hiding this comment.
var [](start = 108, length = 3)
We don't allow usage of var like that. I suggest declaring a local before the if and do (value = NullableAlwaysHasValue((BoundExpression)loweredExpr)) != null instead #Closed
| constantValueOpt: null, initializerExpressionOpt: null, binderOpt: null, type: loweredExpr.Type); | ||
| } | ||
|
|
||
| return EvaluateSideEffectingArgumentToTemp(loweredExpr, initEffects, ref temps); |
There was a problem hiding this comment.
return EvaluateSideEffectingArgumentToTemp(loweredExpr, initEffects, ref temps); [](start = 12, length = 80)
We should also optimize NullableNeverHasValue case, an expression like that doesn't need caching #Closed
There was a problem hiding this comment.
I don't think it needs to be handled specially.
There are two cases:
- an element that never has value; then it has no side effects, and will not be saved to a temp anyways, and the lowering for element-wise binary operator will handle it,
- a tuple that never has value: this case could be further optimized, but the way of getting that scenario is very contrived and the complexity that adds to the code for rewriting nested case does not seem worthwhile to me. #Closed
There was a problem hiding this comment.
an element that never has value; then it has no side effects, and will not be saved to a temp anyways
It is not clear what makes you think this. Have you tried testing with something other than null or default?
a tuple that never has value: this case could be further optimized, but the way of getting that scenario is very contrived and the complexity that adds to the code for rewriting nested case does not seem worthwhile to me.
I don't believe this will add much complexity. Somehow TestEqualOnNullableVsNullableTuples_TupleAlwaysNull screams at me that we should do better. For whatever reason we thought that handling cases like this specially for regular comparisons is worthwhile, I cannot think of any reason why this should be different for tuple comparison. Also, consistency is a good thing.
In reply to: 176383420 [](ancestors = 176383420)
There was a problem hiding this comment.
I've tried multiple expressions, such as new and casts. The lowered node for elements that never have a value is a bound default expression, which is recognized have having no side effects.
I agree that the generated IL could be simpler (that's why I included that test).
In terms of complexity, I had tried this over the weekend and found it complex (there were repercussions which I don't remember the details of), especially in the context of this rewriting which is already not simply. I'd rather add a comment to mention the "never null" case.
I can try again, but frankly I feel this is a waste of time, as writing nullable tuples that are always null is contrived (cast or new) in the context of tuple equality.
In reply to: 176505498 [](ancestors = 176505498,176383420)
There was a problem hiding this comment.
Ok, I think the problem is that I wanted to skip computing the GetValueOrDefault portion in that case, but I can leave it in (it will be optimized away anyways).
In reply to: 176557640 [](ancestors = 176557640,176505498,176383420)
| // that temp so it can be recognized as always having a value later on. | ||
| var savedValue = EvaluateSideEffectingArgumentToTemp(value, initEffects, ref temps); | ||
| SyntaxNode syntax = loweredExpr.Syntax; | ||
| _ = TryGetSpecialTypeMethod(syntax, SpecialMember.System_Nullable_T__ctor, out MethodSymbol nullableCtor); |
There was a problem hiding this comment.
_ = TryGetSpecialTypeMethod(syntax, SpecialMember.System_Nullable_T__ctor, out MethodSymbol nullableCtor); [](start = 15, length = 107)
I think, there is no need to go through all this trouble. Just update the argument in, loweredExpr, it is already an object creation, NullableAlwaysHasValue made sure of that. Especially that we are not supposed to ignore value returned by TryGetSpecialTypeMethod #Closed
| else | ||
| { | ||
| rightHasValue = MakeBooleanConstant(right.Syntax, true); | ||
| rightValue = value; |
There was a problem hiding this comment.
rightValue = value; [](start = 20, length = 19)
isRightNullable = false; ? #Closed
There was a problem hiding this comment.
It is used on line 201 below if (!isLeftNullable && !isRightNullable)
In reply to: 176278184 [](ancestors = 176278184)
There was a problem hiding this comment.
| else | ||
| { | ||
| leftHasValue = MakeBooleanConstant(left.Syntax, true); | ||
| leftValue = value; |
There was a problem hiding this comment.
leftValue = value; [](start = 20, length = 18)
isLeftNullable = false; ? #Closed
There was a problem hiding this comment.
isLeftNullable isn't used below. #Closed
There was a problem hiding this comment.
isLeftNullable isn't used below.
It is used on line 201 below if (!isLeftNullable && !isRightNullable)
In reply to: 176383909 [](ancestors = 176383909)
| leftHasValue = MakeHasValueTemp(left, temps, outerEffects); | ||
| leftValue = MakeValueOrDefaultTemp(left, temps, innerEffects); | ||
| BoundExpression value = NullableAlwaysHasValue(left); | ||
| if (value is null) |
There was a problem hiding this comment.
if (value is null) [](start = 16, length = 18)
Should also handle NullableNeverHasValue cases #Closed
| IL_0001: ldloca.s V_3 | ||
| IL_0003: dup | ||
| IL_0004: initobj ""(int, int)?"" | ||
| IL_000a: call ""bool (int, int)?.HasValue.get"" |
There was a problem hiding this comment.
""bool (int, int)?.HasValue.get"" [](start = 23, length = 33)
I'd expect to see much simpler IL. #Closed
There was a problem hiding this comment.
|
Done with review pass (iteration 1) #Closed |
| // with a temp saving that value | ||
| BoundExpression savedValue = EvaluateSideEffectingArgumentToTemp(value, initEffects, ref temps); | ||
| var objectCreation = (BoundObjectCreationExpression)loweredExpr; | ||
| return objectCreation.UpdateArgumentsAndInitializer(ImmutableArray.Create(savedValue), objectCreation.ArgumentRefKindsOpt, objectCreation.InitializerExpressionOpt); |
There was a problem hiding this comment.
It is not a contract of NullableAlwaysHasValue that it only returns true when the argument is an object creation expression. I expect, for example, that we would expand it to also include certain conversions on top of a creation expression, as is done by the extension method of the same name. #Resolved
There was a problem hiding this comment.
Alternately, if that is a contract of NullableAlwaysHasValu, it should be documented so we don't break it in the future.
In reply to: 176492220 [](ancestors = 176492220)
There was a problem hiding this comment.
Alternately, if that is a contract of NullableAlwaysHasValu, it should be documented so we don't break it in the future.
I agree with that, let's add a comment in NullableAlwaysHasValue saing that if it starts returning true for more complicated nodes, the logic here would have to be adjusted accordingly.
In reply to: 176493243 [](ancestors = 176493243,176492220)
| { | ||
| static void Main() | ||
| { | ||
| System.Console.Write($""{(new int?(Identity(42)), 2) == (new int?(42), 2)} ""); |
There was a problem hiding this comment.
Please also try the same thing with a cast to int? instead of a new. #Resolved
|
Done with review pass (iteration 2) #Closed |
| verifier.VerifyIL("C.Main", @"{ | ||
| // Code size 13 (0xd) | ||
| .maxstack 2 | ||
| .locals init (System.ValueTuple<int, int> V_0, |
There was a problem hiding this comment.
.locals init (System.ValueTuple<int, int> V_0, [](start = 2, length = 46)
None of the locals are used, it would be good to confirm that they are going to be removed for release build. #Closed
There was a problem hiding this comment.
| System.ValueTuple<int, int> V_1, | ||
| (int, int)? V_2) | ||
| IL_0000: nop | ||
| IL_0001: br.s IL_0003 |
There was a problem hiding this comment.
br.s IL_0003 [](start = 11, length = 19)
There are some unnecessary jumps, it would be good to confirm that they are going to be removed for release build. #Closed
| { | ||
| public static void Main() | ||
| { | ||
| System.Console.Write(((int, int)?)null == ((int, int)?)null); |
There was a problem hiding this comment.
((int, int)?)null == ((int, int)?)null [](start = 29, length = 38)
It would be good to cover all combinations: left (might be null, never null, always null) vs. right (never null, always null). And switch sides too. #Closed
AlekseyTs
left a comment
There was a problem hiding this comment.
LGTM (iteration 5) modulo some test suggestions
| } | ||
| "; | ||
|
|
||
| CompileAndVerify(source, expectedOutput: "True", options: TestOptions.ReleaseExe).VerifyIL("C.M", @"{ |
There was a problem hiding this comment.
📝 I'll take a stab at optimizing this further. #Resolved
| IL_0007: nop | ||
| IL_0008: ret | ||
| } | ||
| "); |
There was a problem hiding this comment.
📝 The never-null vs. never-null is covered by TestEqualOnNullableVsNullableTuples_NeverNull.
The maybe-null vs. maybe-null is covered by many tests in this file. #Resolved
|
test windows_release_unit32_prtest please |
|
test windows_debug_unit64_prtest please |
|
@jaredpar for ask-mode approval for 15.7. Thanks |
|
test windows_debug_unit64_prtest please |
|
approved. |
…features/dataflow * dotnet/dev15.7.x: Fix inference when tuple names differ (dotnet#25738) Avoid crash with 'new ref[]' (dotnet#25505) Skip some PDBUsingTests tests to unblock CI (dotnet#25753) Disable Spanish queue Tuple equality: Optimize cases where nullable always has value (dotnet#25644) Bump prerelease version to beta4 Fix failure to initialize RemoteWorkspace in the out-of-process host remove whitespace Removing package dependency for Microsoft.VisualStudio.Text.UI.Wpf in EditorFeatures Removing package dependency for Microsoft.VisualStudio.Text.UI.Wpf in CSharpEditorFeatures
Customer scenario
Optimize the tuple-equality case where a nullable tuple or nullable element is known to never be null.
Bugs this fixes
Fixes #25487
Risk
Performance impact
Low. The change is only affecting tuple equality lowering.
Is this a regression from a previous update?
No