Perf: Reimplement Lookup.Scope tables without ItemDictionary#12320
Merged
YuliiaKovalova merged 4 commits intodotnet:mainfrom Sep 30, 2025
Merged
Conversation
Contributor
There was a problem hiding this comment.
Pull Request Overview
This PR optimizes performance by replacing the ItemDictionary implementation in Lookup.Scope with a lighter-weight ItemDictionarySlim, reducing CPU usage and memory allocations. The changes eliminate locking overhead and simplify data structures used for tracking item additions, removals, and empty markers within lookup scopes.
- Introduces
ItemDictionarySlimto replaceItemDictionaryfor scope-specific item tracking - Replaces empty markers with a frozen set-based truncation approach using
ItemTypesToTruncateAtThisScope - Optimizes item merging and removal logic to avoid duplicate checking and intermediate allocations
Reviewed Changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| Lookup.cs | Core performance optimization replacing ItemDictionary with ItemDictionarySlim and implementing new truncation mechanism |
| ItemDictionary.cs | Removes unused methods (AddRange, Replace, AddEmptyMarker, HasEmptyMarker) to simplify API |
| IItemDictionary.cs | Updates interface by removing unused method signatures |
| ImmutableItemDictionary.cs | Removes overridden implementations of deleted interface methods |
| ItemBucket.cs | Updates to use new truncation API instead of empty markers |
| BatchingEngine.cs | Changes to use FrozenSet for item names in bucket creation |
| TargetEntry.cs | Adds explicit truncation calls for incremental build scenarios |
| TargetUpToDateChecker.cs | Replaces AddEmptyMarker calls with ImportItemsOfType |
| ItemGroupIntrinsicTask.cs | Updates RemoveItems call signature to include item type parameter |
| Test files | Updates test code to work with new APIs and data structures |
b5d1045 to
82177b1
Compare
YuliiaKovalova
approved these changes
Sep 29, 2025
surayya-MS
approved these changes
Sep 29, 2025
Member
surayya-MS
left a comment
There was a problem hiding this comment.
Looks good to me!
@ccastanedaucf let me know if you would like to merge it as is or address comments
YuliiaKovalova
pushed a commit
that referenced
this pull request
Oct 27, 2025
**NOTE:** - ~First couple commits are duplicated from #12320 - it removes some `ItemDictionary` APIs that simplify this refactor by eliminating most use cases for the O(1) lookup, so just ignore them for now.~ Force pushed the rebased version. ### Context After #12320 , the O(1) `LinkedListNode` lookup in `ItemDictionary` is only used for removes. If removes are batched by the same item type (see new `RemoveItemsByItemType()`, the cost to build a `HashSet` lookup on-demand is trivial compared to the current approach of maintaining an additional O(1) lookup table with every dictionary entry - especially given that removes are only used in a few scenarios. ### Perf Working set memory at end of build *(-17% and nearly the entire Large Object Heap)*: **Before** <img width="360" height="178" alt="image" src="https://github.com/user-attachments/assets/b8cb5ffd-cfdf-47d2-bc56-ca19b7f6e60e" /> **After** <img width="386" height="180" alt="image" src="https://github.com/user-attachments/assets/522ddb3b-f39f-464c-ad1d-551c32f4f42f" /> Total allocations *(-300MB, finally no longer double digits!)*: **Before** <img width="1202" height="470" alt="image" src="https://github.com/user-attachments/assets/f048bf29-ea0c-4768-abd0-b2e8d6570f3e" /> **After** <img width="1202" height="406" alt="image" src="https://github.com/user-attachments/assets/691983f4-8d04-44f7-a5cb-78ba12ec4e7f" /> This one also has a pretty significant drop in total GC time relative to allocations, I'm guessing due to the difference in LOH? I compared 4 profiles here to make sure this wasn't noise. **Before** <img width="1178" height="460" alt="image" src="https://github.com/user-attachments/assets/ee20aabd-a27e-4002-9dfa-7ca70b3ecb2c" /> **After** <img width="1226" height="422" alt="image" src="https://github.com/user-attachments/assets/bb883650-fd76-4e45-b544-d2df68770d40" /> This also further reduces CPU when merging down to the base Scope. Much of the remaining cost here was related to updating the additional dictionary, and this is also the only location where batch removes occur. **Before** <img width="913" height="169" alt="image" src="https://github.com/user-attachments/assets/a39c91a5-6c6f-4d17-b0eb-9b549f98e475" /> **After** <img width="920" height="221" alt="image" src="https://github.com/user-attachments/assets/42496d81-b22f-45a3-bfaa-458c19e7f186" />
This was referenced Feb 23, 2026
Merged
Open
This was referenced Mar 2, 2026
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.
Fixes
Reduces a significant amount of CPU and allocations in
Lookuprelated toItemDictionaryoverhead.Before:

After (-2.8s of CPU - most of the remaining time is related to metadata

Modifies,PropertyDictionary, and the outer scope which still usesItemDictionary)Allocations are harder to 1:1 granularly, but you can see on the left a total ~870MB reduction.
Before:

After (-870MB):

Context
Lookupuses a stack ofScopeobjects which each track multiple optional tables ofProjectItemInstance(implemented viaItemDictionary).For the context of the PR, here's a quick overview of how this currently works:
Itemsis merged into the next scope. After this point, the popped scope is effectively discarded (seeMergeScopeIntoNotLastScope()).Items(seeMergeScopeIntoLastScope()). Unlike other scopes, this base table is externally provided at the construction of the firstLookup.GetItems()returns a non-destructive merge down the scope stack, stopping if either the outer scope is reached, or a scope is found with itsItemstable set.ItemDictionary, even though we only return a single item type list.Itemstables generally only contain one item type with items, with others containing "empty markers". These are used to mask the base item dictionary, primarily inItemBucketwhere each bucket (and therefore scope) holds a single type.Currently, each of these intermediate tables are implemented via
ItemDictionary, which internally uses aDictionary<string, LinkedList>to represent item lists and aDictionary<ProjectItemInstance, LinkedList>to provide O(1) access to indices. Empty markers are implemented by creating empty entries. Every method also takes a lock, sinceItemDictionaryis designed for use in other areas of MSBuild - whereasLookupis only used in single-threaded contexts.This all adds overhead that can be avoided by replacing
ItemDicitonarywith basic collections for all inner scopes.Changes Made
ItemDictionarySlimto replaceItemDictionaryinside ofLookup, while still providing similar helper methods.ItemTypesToTruncateAtThisScopeGetItems