Skip to content

Tuple equality: Optimize cases where nullable always has value#25644

Merged
jcouv merged 7 commits intodotnet:dev15.7.xfrom
jcouv:never-null
Mar 26, 2018
Merged

Tuple equality: Optimize cases where nullable always has value#25644
jcouv merged 7 commits intodotnet:dev15.7.xfrom
jcouv:never-null

Conversation

@jcouv
Copy link
Copy Markdown
Member

@jcouv jcouv commented Mar 21, 2018

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

@jcouv jcouv added this to the 15.7 milestone Mar 21, 2018
@jcouv jcouv self-assigned this Mar 21, 2018
@jcouv jcouv requested a review from a team as a code owner March 21, 2018 04:50
@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Mar 21, 2018

@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))
Thanks #Resolved

@jcouv jcouv changed the title Optimize cases where nullable always has value Tuple equality: Optimize cases where nullable always has value Mar 21, 2018
// 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)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 21, 2018

Choose a reason for hiding this comment

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

(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)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 21, 2018

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 22, 2018

Choose a reason for hiding this comment

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

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);
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 22, 2018

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member Author

@jcouv jcouv Mar 22, 2018

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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);
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 22, 2018

Choose a reason for hiding this comment

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

_ = 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;
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 22, 2018

Choose a reason for hiding this comment

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

rightValue = value; [](start = 20, length = 19)

isRightNullable = false; ? #Closed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It is used on line 201 below if (!isLeftNullable && !isRightNullable)


In reply to: 176278184 [](ancestors = 176278184)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah, thanks


In reply to: 176501791 [](ancestors = 176501791,176278184)

else
{
leftHasValue = MakeBooleanConstant(left.Syntax, true);
leftValue = value;
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 22, 2018

Choose a reason for hiding this comment

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

leftValue = value; [](start = 20, length = 18)

isLeftNullable = false; ? #Closed

Copy link
Copy Markdown
Member Author

@jcouv jcouv Mar 22, 2018

Choose a reason for hiding this comment

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

isLeftNullable isn't used below. #Closed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 22, 2018

Choose a reason for hiding this comment

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

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""
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 22, 2018

Choose a reason for hiding this comment

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

""bool (int, int)?.HasValue.get"" [](start = 23, length = 33)

I'd expect to see much simpler IL. #Closed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Like for the test below


In reply to: 176297764 [](ancestors = 176297764)

@AlekseyTs
Copy link
Copy Markdown
Contributor

AlekseyTs commented Mar 22, 2018

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);
Copy link
Copy Markdown
Member

@gafter gafter Mar 22, 2018

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

@gafter gafter Mar 22, 2018

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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)} "");
Copy link
Copy Markdown
Member

@gafter gafter Mar 22, 2018

Choose a reason for hiding this comment

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

Please also try the same thing with a cast to int? instead of a new. #Resolved

@AlekseyTs
Copy link
Copy Markdown
Contributor

AlekseyTs commented Mar 22, 2018

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,
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 23, 2018

Choose a reason for hiding this comment

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

.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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

They're still there.


In reply to: 176801873 [](ancestors = 176801873)

System.ValueTuple<int, int> V_1,
(int, int)? V_2)
IL_0000: nop
IL_0001: br.s IL_0003
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 23, 2018

Choose a reason for hiding this comment

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

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);
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Mar 23, 2018

Choose a reason for hiding this comment

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

((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

Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (iteration 5) modulo some test suggestions

}
";

CompileAndVerify(source, expectedOutput: "True", options: TestOptions.ReleaseExe).VerifyIL("C.M", @"{
Copy link
Copy Markdown
Member Author

@jcouv jcouv Mar 23, 2018

Choose a reason for hiding this comment

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

📝 I'll take a stab at optimizing this further. #Resolved

IL_0007: nop
IL_0008: ret
}
");
Copy link
Copy Markdown
Member Author

@jcouv jcouv Mar 23, 2018

Choose a reason for hiding this comment

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

📝 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

Copy link
Copy Markdown
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

:shipit:

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Mar 23, 2018

test windows_release_unit32_prtest please

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Mar 23, 2018

test windows_debug_unit64_prtest please

Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

LGTM (iteration 7)

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Mar 26, 2018

@jaredpar for ask-mode approval for 15.7. Thanks

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Mar 26, 2018

test windows_debug_unit64_prtest please

@jaredpar
Copy link
Copy Markdown
Member

approved.

@jcouv jcouv merged commit 1f3c89e into dotnet:dev15.7.x Mar 26, 2018
@jcouv jcouv deleted the never-null branch March 26, 2018 19:31
333fred added a commit to 333fred/roslyn that referenced this pull request Mar 28, 2018
…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
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