Avoid boxing allocation of Metadata collection#13271
Merged
JanProvaznik merged 2 commits intodotnet:mainfrom Feb 20, 2026
Merged
Avoid boxing allocation of Metadata collection#13271JanProvaznik merged 2 commits intodotnet:mainfrom
JanProvaznik merged 2 commits intodotnet:mainfrom
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes performance by eliminating boxing allocations when iterating over metadata elements in MSBuild's evaluation path. The optimization addresses a hot path where foreach over itemElement.Metadata causes the struct enumerator to be boxed due to the ICollection<T> interface. By introducing an internal MetadataEnumerable property that returns the concrete collection type, the compiler can use the public struct GetEnumerator() directly, avoiding the allocation.
Changes:
- Added internal
MetadataEnumerableproperty toProjectItemElementthat returns the concreteProjectElementSiblingSubTypeCollection<ProjectMetadataElement>type - Updated
LazyItemEvaluator.ProcessMetadataElementsto useMetadataEnumerableinstead ofMetadatato avoid boxing
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/Build/Construction/ProjectItemElement.cs | Adds internal MetadataEnumerable property following the established Children/ChildrenEnumerable pattern to expose concrete collection type |
| src/Build/Evaluation/LazyItemEvaluator.cs | Updates hot path in ProcessMetadataElements to use MetadataEnumerable instead of Metadata to eliminate struct enumerator boxing |
JanProvaznik
approved these changes
Feb 20, 2026
JanProvaznik
pushed a commit
to JanProvaznik/msbuild
that referenced
this pull request
Feb 25, 2026
**🤖 AI-Generated Pull Request 🤖** 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](https://devdiv.visualstudio.com/DevDiv/_wiki/wikis/DevDiv.wiki/49206/PerfRel-Agent). Please share feedback with [TIP Insights](mailto:tipinsights@microsoft.com) - Issue: `ProcessMetadataElements` iterates over `itemElement.Metadata`, which returns `ICollection<ProjectMetadataElement>`. The underlying `ProjectElementSiblingSubTypeCollection<T>` exposes a zero-alloc public struct `Enumerator`, but the `ICollection<T>` return type forces `foreach` through the explicit `IEnumerable<T>.GetEnumerator()` interface implementation, boxing the struct enumerator on every call. This fires once per item element with metadata during evaluation — a hot path in large project builds. This matches the allocation stack flow showing `Evaluate` → `EvaluateItemGroupElement` → `EvaluateItemElement` → `ProcessItemElement` → `BuildIncludeOperation` → `ProcessMetadataElements` → `IEnumerable<T>.GetEnumerator` → `JIT_New` → `SVR::GCHeap::Alloc` → `TypeAllocated!Enumerator[ProjectMetadataElement]` ``` microsoft.build.dll!LazyItemEvaluator.ProcessItemElement microsoft.build.dll!LazyItemEvaluator.BuildIncludeOperation microsoft.build.dll!LazyItemEvaluator.ProcessMetadataElements microsoft.build.dll!ProjectElementSiblingSubTypeCollection`1.System.Collections.Generic.IEnumerable<T>.GetEnumerator clr.dll!JIT_New clr.dll!SVR::GCHeap::Alloc clr.dll!SVR::gc_heap::try_allocate_more_space TypeAllocated!Enumerator[Microsoft.Build.Construction.ProjectMetadataElement] ``` - Issue type: Eliminate unnecessary struct enumerator boxing allocation in hot evaluation path - Proposed fix: Follow the established `Children` / `ChildrenEnumerable` pattern already in `ProjectElementContainer` — add an `internal MetadataEnumerable` property on `ProjectItemElement` that returns the concrete `ProjectElementSiblingSubTypeCollection<ProjectMetadataElement>` type, placed directly next to the public `Metadata` property. Both delegate to the same `GetChildrenOfType<ProjectMetadataElement>()` call. The consumer in `LazyItemEvaluator.ProcessMetadataElements` is updated to use `MetadataEnumerable`, allowing `foreach` to resolve the public struct `GetEnumerator()` directly and eliminating the boxing allocation. [Best practices wiki](https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/24181/Garbage-collection-(GC)) [See related failure in PRISM](https://prism.vsdata.io/failure/?eventType=allocation&failureType=dualdirection&failureHash=505520c2-9b99-3dc0-9f15-5b69762959c5) [ADO work item](https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2742617) Co-authored-by: Naresh Joshi <Naresh.Joshi@microsoft.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 AI-Generated Pull Request 🤖
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
ProcessMetadataElementsiterates overitemElement.Metadata, which returnsICollection<ProjectMetadataElement>.The underlying
ProjectElementSiblingSubTypeCollection<T>exposes a zero-alloc public structEnumerator, but theICollection<T>return type forcesforeachthrough the explicitIEnumerable<T>.GetEnumerator()interface implementation, boxing the struct enumerator on every call.This fires once per item element with metadata during evaluation — a hot path in large project builds.
This matches the allocation stack flow showing
Evaluate→EvaluateItemGroupElement→EvaluateItemElement→ProcessItemElement→BuildIncludeOperation→ProcessMetadataElements→IEnumerable<T>.GetEnumerator→JIT_New→SVR::GCHeap::Alloc→TypeAllocated!Enumerator[ProjectMetadataElement]Issue type: Eliminate unnecessary struct enumerator boxing allocation in hot evaluation path
Proposed fix: Follow the established
Children/ChildrenEnumerablepattern already inProjectElementContainer— add aninternal MetadataEnumerableproperty onProjectItemElementthat returns the concreteProjectElementSiblingSubTypeCollection<ProjectMetadataElement>type, placed directly next to the publicMetadataproperty.Both delegate to the same
GetChildrenOfType<ProjectMetadataElement>()call. The consumer inLazyItemEvaluator.ProcessMetadataElementsis updated to useMetadataEnumerable, allowingforeachto resolve the public structGetEnumerator()directly and eliminating the boxing allocation.Best practices wiki
See related failure in PRISM
ADO work item