[Efficiency Improver] perf: eliminate state-machine allocations in PropertyBag.Single(T)() and SingleOrDefault(T)()#7857
Merged
Evangelink merged 1 commit intoApr 27, 2026
Conversation
…and SingleOrDefault<T>() Replace triple-LINQ enumeration in Single<T>() and yield-iterator enumeration in SingleOrDefault<T>() with direct linked-list walks. Single<T>() previously called Any(), Skip(1).Any(), and First() on the same IEnumerable returned by Property.OfType<T>() — allocating up to four separate yield-state-machine objects per call. SingleOrDefault<T>() previously called Property.OfType<T>() and then GetEnumerator() on the result — allocating one state-machine object per call. Both methods now walk the Property linked list directly (the same pattern already used by Property.Any<T>() and Property.Contains()), allocating zero heap objects in the common case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR optimizes PropertyBag.Single<TProperty>() and PropertyBag.SingleOrDefault<TProperty>() in Microsoft.Testing.Platform by removing LINQ/yield-based enumeration paths that were creating iterator state-machine allocations, replacing them with direct linked-list traversal over the internal Property nodes.
Changes:
- Replaced
SingleOrDefault<TProperty>()’sOfType<TProperty>()+ enumerator usage with a direct linked-list walk that tracks a single match and throws on duplicates. - Replaced
Single<TProperty>()’s multi-enumeration LINQ pattern (Any(),Skip(1).Any(),First()) with a single-pass linked-list walk. - Added explanatory comments describing the allocation avoidance rationale.
Show a summary per file
| File | Description |
|---|---|
src/Platform/Microsoft.Testing.Platform/Messages/PropertyBag.cs |
Removes iterator/LINQ-based enumeration from Single* APIs and replaces it with allocation-free linked-list traversal. |
Copilot's findings
- Files reviewed: 1/1 changed files
- Comments generated: 1
Member
|
Is this on a hot path? In what scenarios and how often, with how big dataset is this being called? We have a tradeoff of code brevity + expressiveness Versus perf - let's make sure we prefer perf if it really has impact |
JanKrivanek
approved these changes
Apr 27, 2026
Member
JanKrivanek
left a comment
There was a problem hiding this comment.
Approving - but please validate this is a change on hot path, with real perf impact
This was referenced Apr 28, 2026
Closed
2 tasks
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.
Replace triple-LINQ enumeration in Single() and yield-iterator enumeration in SingleOrDefault() with direct linked-list walks.
Single() previously called Any(), Skip(1).Any(), and First() on the same IEnumerable returned by Property.OfType() — allocating up to four separate yield-state-machine objects per call.
SingleOrDefault() previously called Property.OfType() and then GetEnumerator() on the result — allocating one state-machine object per call.
Both methods now walk the Property linked list directly (the same pattern already used by Property.Any() and Property.Contains()), allocating zero heap objects in the common case.
Fixes #7854