Avoid unnecessary red node allocations in ReduceOneNodeOrTokenAsync by replacing .Single() with .First()#82711
Conversation
…y replacing .Single() with .First()
|
Makes sense to me, but someone from Roslyn should take a look. @jasonmalinowski |
|
We should have a debug assert that this only has one item in it. It's fine then to switch the code to be First. That said. isntead of explicitly getting the children and finding the one with the annotation, can we not just directly ask for the annotated node/token? I thought there was a helper on nodes for that. That seems better as it can avoid the greens entirely, not creating the red versions for the children before the match. |
|
The code should change to: that said, i looked at GetAnnotatedNodesAndTokens and it could be more efficient. Right now it is: return this.DescendantNodesAndTokensAndSelf(n => n.ContainsAnnotations, descendIntoTrivia: true)
.Where(t => t.HasAnnotation(annotation));this will realize the red children of a node unnecessarily so that it can call @nareshjo what perf hit do you see here, and hwat perf benefit did you observe from this change? |
It feels like we should have a replace method that returns the new node in the tree, and not require the caller to do this annotation dance.
This would be good to know. For example, there is another Single call a couple lines below the one changed that looks to be walking the full set of descendants from the document root, which I would this is way worse than just realizing a single node's children. |
|
@CyrusNajmabadi, @ToddGrun, thanks for reviewing. This is 18th highest allocation contributor for latest VS releases with allocation rates of 305 MB/s at 50th percentile and 496 at 90th. It also shows up in list of high CPU usage. Allocation/CPU issues affect the responsiveness of VS in general. Just to give a bit of context, this PR came from our internal Perf AI agent which tries to address issues identified in latest VS releases based on perf telemetry. I submitted it just because of some gaps in that system ATM regarding submissions to GitHub repos. Since this is currently a fairly high-ranking allocation and CPU issue as per real-world data, the idea here is to land the low-risk improvement first and leave any broader follow-up optimisations open if reviewers think they’re worth pursuing. I ran some benchmarks with original code and proposed fix with the help of profiler copilot and BenchmarkDotnet (something we are trying to incorporate and include in the PR descriptions from agent upfront to help reviewers better understand impact of change). Allocation reduction
Perf speedup
One option could be to take a simpler yet effective optimisation like this with any other minor tweaks as you see appropriate and if this issue still shows up as high allocation/CPU issue in future releases then try to do more extensive optimisations. |
@ToddGrun |
Would someone on Roslyn be able to help check that? I am not familiar with process to set up a test environment and do that validation. Or I can give it a try if you can point me to any docs which cover that. |
This pull request was generated by the VS Perf Rel AI Agent. Please review this AI-generated PR with extra care! For more information, visit our wiki. Please share feedback with TIP Insights
Issue:
AbstractSimplificationService.ReduceOneNodeOrTokenAsyncuses.Single()onChildNodesAndTokens()which forces enumeration of ALL children of a parent node to verify uniqueness, creating red syntax node wrappers (ExpressionStatementSyntax) for every child viaGetRedElement→CreateRed, even after the matching annotated child has already been found.Evidence:
TypeAllocated!Microsoft.CodeAnalysis.CSharp.Syntax.ExpressionStatementSyntax(red node wrapper).ChildNodesAndTokens().Single(c => c.HasAnnotation(annotation))which enumerates all remaining children after the match to verify uniquenessnew SyntaxAnnotation()which is guaranteed unique by construction (_idassigned viaInterlocked.Increment), making the uniqueness check redundantParallel.ForEachAsync×foreach reducers×do-while HasMoreWorkIssue type: AVOID unnecessary full enumeration when searching for a guaranteed-unique element on hot path
Proposed fix: Change
.Single()to.First()on line 224 to stop enumeration at the first match instead of continuing through all remaining children.The annotation is guaranteed unique by construction (
new SyntaxAnnotation()assigns a unique_idviaInterlocked.Increment), so.First()returns the same element as.Single().This is a minimal and safe fix that eliminates red node allocations for all children after the match while maintaining identical behavior.
Best practices wiki
See related failure in PRISM
ADO work item
Microsoft Reviewers: Open in CodeFlow