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

Several changes to reduce allocations #11273

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

Erarndt
Copy link

@Erarndt Erarndt commented Jan 13, 2025

Fixes #

Context

There is an appreciable amount of time all of the MSBuild process nodes spend doing GC during a build. Taking steps to reduce allocations. This PR contains a mix of approaches that reduce allocations in several areas.

Changes Made

Testing

Notes

@JanKrivanek
Copy link
Member

Related to #11160

@JanKrivanek JanKrivanek self-requested a review January 15, 2025 17:34
/// </example>
/// <typeparam name="T">The item type that is enumerated.</typeparam>
/// <param name="collection">The collections that will be enumerated.</param>
/// <returns>The enumerator for the collection.</returns>
Copy link
Member

Choose a reason for hiding this comment

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

If foreach can be optimised this way - shouldn't it be something that should be part of compiler/runtime? Are we possible missing on any already existing opportunity there? @stephentoub?

Or is this bringing benefit only on NetFx? (in which case we migh possibly want to conditionaly compile it only for NetFx?)

Copy link
Member

Choose a reason for hiding this comment

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

This appears to be trying to manually special-case a few specific collection types by maintaining a discriminated union of all of their strongly-typed struct enumerators. I would not be surprised if this actually makes things more expensive in various situations, especially on .NET Core where dynamic PGO (on by default as of .NET 8) will already do such special-casing and handle devirtualizing and inlining the enumerator dispatches. In .NET 10 the enumerator allocation itself will also frequently be avoided thanks to stack allocation / escape analysis work currently being done by @AndyAyersMS.

Copy link
Author

Choose a reason for hiding this comment

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

The intent with this helper is to selectively apply it in situations where the allocations are significant, so I'd be hesitant to unconditionally apply it for reasons that @stephentoub mentions. Anecdotally, I've applied this change in a couple of places and reverted the change because I saw an increase in CPU cost that wasn't offset by the reduction in allocations.

Copy link
Member

Choose a reason for hiding this comment

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

Gotcha.
Sounds helpful on NetFX. We should probably measure impact on dotnet build (core) though

Copy link
Member

@davkean davkean Feb 4, 2025

Choose a reason for hiding this comment

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

handle devirtualizing and inlining the enumerator dispatches

Just to reinterate @Erarndt here, MSBuild uses workstation GC, and is often blocked from burning CPU because its stuck behind a single threaded GC performing a Gen0/Gen1 cleaning up these enumerators. While long term we want Server GC for this process which will clean up these allocations with ease, it often better within MSBuild burn additional CPU walking these collections across many threads to avoid the allocation, then it is to be block all threads behind whatever thread triggered the workstation GC. This will be applicable for both .NET Core and .NET Framework, until .NET Core does automatic escape analysis to avoid the allocation.

@JanKrivanek JanKrivanek marked this pull request as ready for review January 28, 2025 11:29
@SimaTian
Copy link
Member

Is the plan to split this PR into several smaller ones still in place please?

Copy link
Member

@JanKrivanek JanKrivanek left a comment

Choose a reason for hiding this comment

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

I went through the whole changese and do not have any strong concerns, but it's a bigger PR - so I want one more more detailed look before I sign-off

foreach (T property in (ICollection<T>)_properties)
ICollection<T> propertiesCollection = (ICollection<T>)_properties;
List<TResult> result = new(propertiesCollection.Count);
if (_properties is RetrievableValuedEntryHashSet<T> hashSet)
Copy link
Member

Choose a reason for hiding this comment

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

Please add a comment that RetrievableValuedEntryHashSet is prefered here since it has custome Enumerator implementation returning struct. Otherwise this change might easily get reverted by possible future refactorings

@@ -522,7 +520,7 @@ private void RegisterResolversManifests(ElementLocation location)
// The collections are never modified after this point.
// So I've made them ReadOnly
_specificResolversManifestsRegistry = specificResolversManifestsRegistry.AsReadOnly();
Copy link
Member

Choose a reason for hiding this comment

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

Why this AsReadOnly() is left and next one is removed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants