Skip to content
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

Evaluator allocation optimization #7056

Conversation

MichalPavlik
Copy link
Contributor

Fixes #6307

Context

Evaluator sometimes allocates List<ProjectRootElement> instances when it's not necessary.

Changes Made

Collection allocation is postponed and empty Enumerable singleton is used to represent empty result.
@drewnoakes I tried to use ImmutableArray, but it produced more allocations - builder is copying underlying array when creating instance of the immutable collection.

Testing

I tracked List<ProjectRootElement> allocations while building simple solution with two empty C# projects (.NET Framework and Core). Number of allocated instances dropped from 264 to 178.

Notes

This optimization looked like good first issue to solve, but complexity of the Evaluator is IMHO high and using out parameters for these collections makes it more difficult to track their flow :)

@dnfadmin
Copy link

dnfadmin commented Nov 22, 2021

CLA assistant check
All CLA requirements met.

@dnfadmin
Copy link

CLA assistant check
Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.

❌ MichalPavlik sign now
You have signed the CLA already but the status is still pending? Let us recheck it.

@Therzok
Copy link
Contributor

Therzok commented Nov 22, 2021

Out of curiosity, instead of having the API allocate the list, why not pass a list to add to, to the API? That avoids all the intermediate collection allocations.

@@ -1428,7 +1428,7 @@ private void EvaluateImportElement(string directoryOfImportingFile, ProjectImpor
{
using (_evaluationProfiler.TrackElement(importElement))
{
List<ProjectRootElement> importedProjectRootElements = ExpandAndLoadImports(directoryOfImportingFile, importElement, out var sdkResult);
IEnumerable<ProjectRootElement> importedProjectRootElements = ExpandAndLoadImports(directoryOfImportingFile, importElement, out var sdkResult);

foreach (ProjectRootElement importedProjectRootElement in importedProjectRootElements)
Copy link
Member

Choose a reason for hiding this comment

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

Changing the type of the variable to IEnumerable has the unfortunate implication of regressing foreach performance. Compare the IL code here:

https://sharplab.io/#v2:EYLgtghglgdgNAFxFANgHwAICYCMBYAKAwAYACDHAFgG5CMBmcrUgYVIG9DTvzGNLSAWQAUAGSgBnBAB4KxAHykUkhAEoOXHloBmAewBOAUwgBjABalhc0hNKwlK1Zq3dOBFx/I4AnMImrad08AX2dSUKDuBnIBADkrHHpZHAUHKXU3T1I9I1MLBLJbe2V0sK1MrJ4KX39ArIitCOCgA

When typed as IEnumerable, List's enumerator is boxed (an extra allocation) and then accessed through the IEnumerator interface (tiny bit slower than direct calls).

Can this optimization be implemented without changing the type, by using a special value of null to represent an empty list for example?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know about value type based enumerator for List<T> and I was thinking about using null to keep specific type. To be honest, I don't know why I rejected this idea so it worth to use it.
I will try to use ImmutableArray and MoveToImmutable as Drew mentioned. Maybe we will not need List at all.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ladipro I changed the code to use nulls.

@drewnoakes
Copy link
Member

@drewnoakes I tried to use ImmutableArray, but it produced more allocations - builder is copying underlying array when creating instance of the immutable collection.

There are several ways to construct ImmutableArray. If you use a builder with correct capacity, then MoveToImmutable there is no wasted allocation.

Changing the type of the variable to IEnumerable has the unfortunate implication of regressing foreach performance.

If you are able to use ImmutableArray then the foreach performance benefit will be maintained.

@MichalPavlik
Copy link
Contributor Author

@Therzok It was one of the solutions I proposed in the issue. The change would be more impactful, because for example ExpandAndLoadImports is adding items to collection conditionally based on containsWildcards value. I wanted to avoid bigger changes and make the PR small.

@MichalPavlik
Copy link
Contributor Author

@drewnoakes When I looked at the description for this method some time ago, I somehow fixed in my mind that it immediately creates new array for Builder. I checked the source and it seems I understood it wrong. I will update my branch with ImmutableArray and measure it again.

@MichalPavlik
Copy link
Contributor Author

@drewnoakes I tried MoveToImmutable, but it requires to know the exact number of elements in advance. So I used null insead of empty enumerable in last commit.

Comment on lines 1854 to 1857
if (projects?.Count > 0)
{
projects = new List<ProjectRootElement>(projects);
}
Copy link
Member

Choose a reason for hiding this comment

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

Why is this needed?

Comment on lines 1859 to 1864
if (sdkResult.AdditionalPaths != null)
{
if (projects == null && sdkResult.AdditionalPaths.Count > 0)
{
projects = new List<ProjectRootElement>();
}
Copy link
Member

Choose a reason for hiding this comment

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

nit:

Suggested change
if (sdkResult.AdditionalPaths != null)
{
if (projects == null && sdkResult.AdditionalPaths.Count > 0)
{
projects = new List<ProjectRootElement>();
}
if (sdkResult.AdditionalPaths?.Count > 0)
{
projects ??= new List<ProjectRootElement>();

Or maybe even:

Suggested change
if (sdkResult.AdditionalPaths != null)
{
if (projects == null && sdkResult.AdditionalPaths.Count > 0)
{
projects = new List<ProjectRootElement>();
}
if (sdkResult.AdditionalPaths?.Count > 0)
{

and new up the List lazily when actually adding to it a few lines below.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh boy! That's an embarrassing refactoring mistake. I'm starting to have selective blindness in this part of code. It's scary that all tests passed... It should be fixed now.

@Forgind Forgind added the merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now. label Nov 23, 2021
@ladipro ladipro merged commit 2e437e9 into dotnet:main Nov 26, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
merge-when-branch-open PRs that are approved, except that there is a problem that means we are not merging stuff right now. performance
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reduce List<ProjectRootElement> allocations in Evaluator.cs
7 participants