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
Remove RemoveUnneededReferences from LamdaRewriter #21367
Conversation
Currently, the lambda rewriter has an early optimization pass in analysis that tries to find all local functions that only capture 'this' and remove references to local functions that do the same. There are two problems with this approach: 1) Generally, removing information from the tree is a bad idea because it hurts further analysis passes that may have needed that information. 2) The optimization strategy itself is very tricky and has a number of complex corner cases. This has lead to bugs, for example dotnet#19033. This PR deletes the current method and adds a new optimization routine at the end of the analysis, operating on assigned scopes and environments rather than removing captured variable analysis. The new optimization is as follows: if we end up with an environment containing only 'this', the environment can be removed, all containing methods can be moved to the top-level type, and all environments which capture the 'this' environment can instead directly capture the 'this' parameter. This produces almost the same results as the previous optimization, but is easier to validate as an algebraic equivalence. Fixes dotnet#19033, dotnet#20577
cf7b209
to
a50a481
Compare
The changes come from the new optimization being less aggressive about moving functions which only capture 'this' to the top level. This appears to be a wash -- some codegen gets slightly better, some gets slightly worse.
a50a481
to
1de4f54
Compare
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.
LGTM - the two comments I have are just wild guesses, no idea if my conclusion is right or not.
IL_0023: ldfld ""int C._x"" | ||
IL_0028: call ""void System.Console.WriteLine(int)"" | ||
IL_002d: ret | ||
IL_001b: ldarg.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.
It looks like we're storing this
in both C.<>c__DisplayClass2_0.<>4__this
as well as the receiver on C.<M>g__L12_0(ref C.<>c__DisplayClass2_0)
. Is that intended?
(It looks like C.<M>g__L12_0(ref C.<>c__DisplayClass2_0)
never touches the captured C C.<>c__DisplayClass2_1.<>4__this
)
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.
Yup, this is a result of a less aggressive optimization. What's going on here is we only eliminate the this
closure if it captures only this
. But both x
and this
are captured in the same scope, so the environment contains both x
and this
, thus the optimization doesn't apply. I haven't found a really simple optimization strategy that works for this case, so I didn't do anything extra here.
If you have any suggestions I could incorporate them, though.
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.
Wait... but it's also lowered onto the containing class. I'm not sure why that is - it seems like if we don't optimize the this
out of the environment, the generated method shouldn't go on the containing class. (And grab the field, instead of the this
parameter of the generated method, if it needs the original this
)
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.
Oh, I think I get it - it is by design. We use the generated this
parameter when we can, but the environment may be passed somewhere that we can't use the generated this
parameter (such as a lambda placed on the environment class).
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.
Right, and finding and ruling out all those corner cases is the tricky bit, so I haven't built up a full pass that handles everything.
@@ -123,7 +123,7 @@ int G() | |||
// Code size 7 (0x7) | |||
.maxstack 1 | |||
.locals init (int V_0) | |||
IL_0000: ldarg.0 | |||
IL_0000: ldarg.1 |
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.
I'm guessing the same thing happened here - capturing this
on both the receiver and a field.
/// a local function which directly or indirectly captures 'this'). | ||
/// Calculated in <see cref="MakeAndAssignEnvironments"/>. | ||
/// </summary> | ||
public bool CapturesThis; |
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.
Why fields instead of properties?
Is there ever a case where this can move from true
to false
? If not would it make sense to validate this in a prooperty setter?
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.
Sounds good. I can do that.
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.
In general though why are you preferring fields here over auto-properties? That seems to go against the style of the compiler.
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.
The original LambdaRewriter was all fields. I was just sticking with the surrounding code style.
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.
Gotcha.
{ | ||
return false; | ||
} | ||
_elements.Insert(index, 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.
Consider the case the Insert
operation throws because it's invalid. That means SetWithInsertionOrder
is now in an inconsistent state: _set
has the value but it's not present in _elements
. Think we need to guard against this and ensure the data type is consistent in the face of bad index
values.
@@ -57,5 +78,7 @@ public IEnumerator<T> GetEnumerator() | |||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); | |||
|
|||
public ImmutableArray<T> AsImmutable() => _elements.ToImmutableArrayOrEmpty(); | |||
|
|||
public T this[int i] => _elements[i]; |
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.
Should this be a ref
return? I'd think so if T
is ever a struct
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.
ArrayBuilder doesn't (and can't) have a ref
returning indexer, so this can't.
if (!_set.Add(value)) | ||
{ | ||
return false; | ||
} |
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.
Blank line.
} | ||
catch | ||
{ | ||
_set.Remove(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.
Wanted to give feedback here about writing tests but it seems like we don't have any tests for SetWithInsertionOrder
.
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.
🎉
@@ -119,6 +119,22 @@ public sealed class Closure | |||
|
|||
public ClosureEnvironment ContainingEnvironmentOpt; | |||
|
|||
private bool _capturesThis; |
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.
Blank line
/// Visit all the closures and return true when the <paramref name="func"/> returns | ||
/// true. Otherwise, returns false. | ||
/// </summary> | ||
public static bool CheckClosures(Scope scope, Func<Scope, Closure, bool> func) |
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.
Is this method used? Can't find a reference in this PR.
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.
In InlineThisOnlyEnvironment
when we see if any ContainingEnvironmentOpt
is non-null.
/// variable. Assigned in | ||
/// <see cref="ComputeLambdaScopesAndFrameCaptures(ParameterSymbol)"/> | ||
/// </summary> | ||
public bool CapturesParent; |
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 this value go to false
after going to true
? If so does it make sense to assert?
As @khyperia pointed out, there are some situations with this optimization scheme where an instance method may receive a struct environment that also contains a pointer to class C
{
void CallMe() {}
struct S
{
C _c;
int _x;
}
int Lowered(ref S s)
{
CallMe();
}
} You'll notice that in the I've done a quick benchmark of the two options, and here are the results:
As you can see, this approach appears to be about 10x faster than going through the field. |
…throw-statement-expression * dotnet/features/ioperation: (41 commits) Update CSharpReplaceMethodWithPropertyService.cs Add VB side of fix. Remove unneeded function. Improve trivia preservation when converting methods into a property. VB side of do-not-simplify-nameof if it changes semantics. Do not simplify to an alias in a nameof if it changes the value of hte nameof. Include System.Runtime.Serialization.Primitives and System.Security.Cryptography.Csp in PortableFacades CoreXT package. (dotnet#21438) Address one more refactoring feedback Address PR feedback Fix possible race conditions in TestExtensionErrorHandler Fix expected test results to properly consider trivia Improve messages when tests fail due to expected text Default to considering trivia during testing Remove RemoveUnneededReferences from LamdaRewriter (dotnet#21367) Enable embedding sources to Windows PDBs (dotnet#21391) re-enabled assert we have disabled Do not insert Microsoft.DiaSymReader.Native (dotnet#21420) Recommend 'case' keyword after a pattern-case-clause. Fix NamedArgumentInParameterOrderWithDefaultValue test for new IOperation output. Resolving merge conflict ...
…-literal-text * dotnet/features/ioperation: Update CSharpReplaceMethodWithPropertyService.cs Add VB side of fix. Remove unneeded function. Improve trivia preservation when converting methods into a property. VB side of do-not-simplify-nameof if it changes semantics. Do not simplify to an alias in a nameof if it changes the value of hte nameof. Include System.Runtime.Serialization.Primitives and System.Security.Cryptography.Csp in PortableFacades CoreXT package. (dotnet#21438) Address one more refactoring feedback Address PR feedback Fix possible race conditions in TestExtensionErrorHandler Fix expected test results to properly consider trivia Improve messages when tests fail due to expected text Default to considering trivia during testing Remove RemoveUnneededReferences from LamdaRewriter (dotnet#21367) Enable embedding sources to Windows PDBs (dotnet#21391) re-enabled assert we have disabled Do not insert Microsoft.DiaSymReader.Native (dotnet#21420) Resolving merge conflict Don't pick a project arbitrarily when navigating to symbols
…tatement-refactor * dotnet/features/ioperation: Update CSharpReplaceMethodWithPropertyService.cs Add VB side of fix. Remove unneeded function. Improve trivia preservation when converting methods into a property. VB side of do-not-simplify-nameof if it changes semantics. Do not simplify to an alias in a nameof if it changes the value of hte nameof. Include System.Runtime.Serialization.Primitives and System.Security.Cryptography.Csp in PortableFacades CoreXT package. (dotnet#21438) Address one more refactoring feedback Address PR feedback Fix possible race conditions in TestExtensionErrorHandler Fix expected test results to properly consider trivia Improve messages when tests fail due to expected text Default to considering trivia during testing Remove RemoveUnneededReferences from LamdaRewriter (dotnet#21367) Enable embedding sources to Windows PDBs (dotnet#21391) re-enabled assert we have disabled Do not insert Microsoft.DiaSymReader.Native (dotnet#21420) Resolving merge conflict Don't pick a project arbitrarily when navigating to symbols
…oalescingexpression-refactor * dotnet/features/ioperation: Update CSharpReplaceMethodWithPropertyService.cs Add VB side of fix. Remove unneeded function. Improve trivia preservation when converting methods into a property. VB side of do-not-simplify-nameof if it changes semantics. Do not simplify to an alias in a nameof if it changes the value of hte nameof. Include System.Runtime.Serialization.Primitives and System.Security.Cryptography.Csp in PortableFacades CoreXT package. (dotnet#21438) Address one more refactoring feedback Address PR feedback Fix possible race conditions in TestExtensionErrorHandler Fix expected test results to properly consider trivia Improve messages when tests fail due to expected text Default to considering trivia during testing Remove RemoveUnneededReferences from LamdaRewriter (dotnet#21367) Enable embedding sources to Windows PDBs (dotnet#21391) re-enabled assert we have disabled Do not insert Microsoft.DiaSymReader.Native (dotnet#21420) Resolving merge conflict Don't pick a project arbitrarily when navigating to symbols
Currently, the lambda rewriter has an early optimization pass in
analysis that tries to find all local functions that only capture 'this'
and remove references to local functions that do the same. There are two
problems with this approach:
This PR deletes the current method and adds a new optimization routine
at the end of the analysis, operating on assigned scopes and
environments rather than removing captured variable analysis. The new
optimization is as follows: if we end up with an environment containing
only 'this', the environment can be removed, all containing methods can
be moved to the top-level type, and all environments which capture the
'this' environment can instead directly capture the 'this' parameter.
This produces almost the same results as the previous optimization, but
is easier to validate as an algebraic equivalence.
Fixes #19033
Fixes #20577