Fix a case where the stack spiller spills more than needed. (possibly resulting in false errors)#24273
Conversation
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.
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.
|
@dotnet/roslyn-compiler - please review. |
|
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. |
| { | ||
| Debug.Assert(refKinds.IsDefault || refKinds.Length == args.Length); | ||
|
|
||
| if(args.Length == 0) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
|
@dotnet-bot test windows_release_vs-integration_prtest please |
|
@dotnet/roslyn-compiler - PING. Can someone take a look? |
| var replacement = Spill(builder, newList[i], refKind, sideEffectsOnly); | ||
|
|
||
| Debug.Assert(sideEffectsOnly || replacement != null); | ||
| Debug.Assert(!sideEffectsOnly || refKind == RefKind.None); |
There was a problem hiding this comment.
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(); | |||
There was a problem hiding this comment.
GetInstance [](start = 55, length = 11)
Can we guess the required size? #Closed
There was a problem hiding this comment.
|
|
||
| <Fact> | ||
| <WorkItem(17706, "https://github.com/dotnet/roslyn/issues/17706")> | ||
| Public Sub SpillingByrefCall_NoSpilling() |
There was a problem hiding this comment.
ByRef, here and in method below. #Resolved
|
CC @MeiChin-Tsai for ask mode approval |
|
approved. |
|
Thanks!! |
Fixes:#17706
Customer scenario
Customer uses code that involves a sequence of argument expressions that includes
await- most commonly it is a method call withawaitamong arguments, bit other more complex cases exist too.Since stack values cannot be preserved across
awaitsuspension, compiler turns the expressions that go before theawaitinto statement forms that store evaluation results in temporaries, which are used to rebuild the argument values after resuming from theawaitsuspension.The problem is that compiler spills one more argument value than needed.
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
awaitin call arguments..Risk
Risk is low since it primarily affects cases where the difference is observable, which is a combination of method calls,
awaitand 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