Skip to content

Fix a case where the stack spiller spills more than needed. (possibly resulting in false errors)#24273

Merged
VSadov merged 6 commits intodotnet:dev15.6.xfrom
VSadov:fixSpill
Jan 19, 2018
Merged

Fix a case where the stack spiller spills more than needed. (possibly resulting in false errors)#24273
VSadov merged 6 commits intodotnet:dev15.6.xfrom
VSadov:fixSpill

Conversation

@VSadov
Copy link
Copy Markdown
Member

@VSadov VSadov commented Jan 17, 2018

Fixes:#17706

Customer scenario

Customer uses code that involves a sequence of argument expressions that includes await - most commonly it is a method call with await among arguments, bit other more complex cases exist too.

Method(a(), b(), await c() + d(), e());  // must have empty stack when `await` happens

Since stack values cannot be preserved across await suspension, compiler turns the expressions that go before the await into statement forms that store evaluation results in temporaries, which are used to rebuild the argument values after resuming from the await suspension.

var t1 = a();
var t2 = b();
var t3 = await c();              // this `await` is ok since the stack is empty
Method(t1, t2, t3 + d(), e()) ;  // <-- should be rewritten like this

The problem is that compiler spills one more argument value than needed.

var t1 = a();
var t2 = b();
var t3 = await c();
var t4 = d();                   // <-- this is redundant at best
Method(t1, t2, t3 + t4, e());    

In some cases it would make no difference (when spilling is a noop, like for constants), in some cases it will result in suboptimal, but correct code, and yet in some cases when argument happen to be unspillable (like a ref-returning member) it may result in a false compile error.

Bugs this fixes

#17706

Workarounds, if any

In some cases the differences are unobservable or an ignorable small degrade. No workaround is needed.

In other cases where it results in a compile error, user may refactor the code to "spill manually" and avoid using await in call arguments..

Risk

Risk is low since it primarily affects cases where the difference is observable, which is a combination of method calls, await and ref-returning members.

Performance impact

Low perf impact because no extra allocations/no complexity changes.

Is this a regression from a previous update?

NO

Root cause analysis

The bug involves a combination of features and in most cases leads to suboptimal, but correct code.

Tests are added for cases where the difference in spilling strategy leads to error/pass distinction.
There are existing tests where the change in code quality can be observed.

How was the bug found?

Customer reported.

Test documentation updated?

N/A

The last await-containing expression in an argument triggers spilling of all argument before it.
The value of the expression itself, however, should not be spilled. (there are no awaits after it)

Agreesive spilling may result in substandard codegenration and/or unexpected errors.
@VSadov VSadov requested a review from a team as a code owner January 17, 2018 01:25
VB does not have this particular bug and corresponding scenario works correctly.
There are however other bugs beyond the scope of what is fixed in this PR.
@VSadov VSadov changed the title [WIP] Fix stack spiller Fix a case where the stack spiller spills more than needed. (possibly resulting in false errors) Jan 17, 2018
@VSadov
Copy link
Copy Markdown
Member Author

VSadov commented Jan 17, 2018

@dotnet/roslyn-compiler - please review.

@VSadov
Copy link
Copy Markdown
Member Author

VSadov commented Jan 17, 2018

Note - VB has a different implementation of stack spiller and it is not affected by this particular bug. A corresponding test has been added and it works as expected.

However VB seems to have other more general issues with spilling and ref-returning members.
A bug logged to track: #24275

{
Debug.Assert(refKinds.IsDefault || refKinds.Length == args.Length);

if(args.Length == 0)
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 17, 2018

Choose a reason for hiding this comment

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

Need a space here between if and ( #Resolved


for (int i = lastSpill + 1; i < newList.Length; i++)
// the value of the last spill and everything that follows is not spilled
if (lastSpill < newList.Length)
Copy link
Copy Markdown
Member

@jaredpar jaredpar Jan 17, 2018

Choose a reason for hiding this comment

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

The use of sideEffectsOnly in this method seems inconsistent. Depending in the content of the newList when sideEffectsOnly is true we will return an empty list, a list which is part of newList or all of newList. That intuitively seems wrong. Am I missing something subtle here? #ByDesign

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.

Spill ensures that replacement is not sideeffecting (it is basically the "rehydrate" code). All sideeffects are moved into the spill builder. Therefore we can drop replacements when we know we only need sideeffects. - like if we are spilling a sequence. In fact some replacements are already null when coming from Spill - Spill knows when we do sideeffectsOnly and may just return null as the replacement as an easy-out when convenient.

This is essentially an optimization, but it is an important one.
If we keep unnecessary replacements, it will look like the values are spilled, survive across await and get restored - only to be unused later...

If we drop replacements right here, the spills become dead stores that do not need to be captured. Furthermore - IL Optimizer will kill dead stores and their locals, so only sideeffects will be emitted.


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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

At the same time though the method name is VisitX and it returns an array of varying length. Generally I would expect VisitX style methods to return an equivalent node. Maybe it's just the name throwing me off.

Copy link
Copy Markdown
Member Author

@VSadov VSadov Jan 19, 2018

Choose a reason for hiding this comment

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

I think the confusing part is that this is a bit unusual rewrite. It is not purely node-to-node. We are also filling up a builder. Parts of the input may move into the builder so it may appear that we have lost something.
Most of the VisitX methods in this rewriter are like that.


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

@VSadov
Copy link
Copy Markdown
Member Author

VSadov commented Jan 17, 2018

@dotnet-bot test windows_release_vs-integration_prtest please

@VSadov
Copy link
Copy Markdown
Member Author

VSadov commented Jan 18, 2018

@dotnet/roslyn-compiler - PING. Can someone take a look?

@jcouv jcouv added this to the 15.6 milestone Jan 18, 2018
var replacement = Spill(builder, newList[i], refKind, sideEffectsOnly);

Debug.Assert(sideEffectsOnly || replacement != null);
Debug.Assert(!sideEffectsOnly || refKind == RefKind.None);
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Jan 18, 2018

Choose a reason for hiding this comment

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

Debug.Assert(!sideEffectsOnly || refKind == RefKind.None); [](start = 16, length = 58)

Can we move this assert to the beginning of the function? Something like:

Debug.Assert(!sideEffectsOnly || refKinds.IsDefault);
``` #Closed

@@ -487,21 +492,33 @@ private ImmutableArray<BoundExpression> VisitExpressionList(
}

var result = ArrayBuilder<BoundExpression>.GetInstance();
Copy link
Copy Markdown
Contributor

@AlekseyTs AlekseyTs Jan 18, 2018

Choose a reason for hiding this comment

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

GetInstance [](start = 55, length = 11)

Can we guess the required size? #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.

newList.Length would be a fair estimate.


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

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)


<Fact>
<WorkItem(17706, "https://github.com/dotnet/roslyn/issues/17706")>
Public Sub SpillingByrefCall_NoSpilling()
Copy link
Copy Markdown
Contributor

@cston cston Jan 18, 2018

Choose a reason for hiding this comment

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

ByRef, here and in method below. #Resolved

@VSadov
Copy link
Copy Markdown
Member Author

VSadov commented Jan 19, 2018

CC @MeiChin-Tsai for ask mode approval

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

@MeiChin-Tsai
Copy link
Copy Markdown

approved.

@VSadov
Copy link
Copy Markdown
Member Author

VSadov commented Jan 19, 2018

Thanks!!

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.

6 participants