Conversation
DocSvartz
approved these changes
Sep 18, 2026
Comment on lines
+102
to
+107
| var firstMembersByName = new Dictionary<string, MemberInfo>(StringComparer.Ordinal); | ||
| foreach (var member in currentTypeMembers) | ||
| { | ||
| if (!firstMembersByName.ContainsKey(member.Name)) | ||
| firstMembersByName.Add(member.Name, member); | ||
| } |
Contributor
There was a problem hiding this comment.
@vb-kalei
As I understand it, this is essentially an deterministic dictionary for the type (Its content will always be the same for each specific type).
Could it perhaps be stored somewhere in the context instead of being calculated from scratch every time?
Contributor
There was a problem hiding this comment.
In this case, it turns out that even within a single call to GetFieldsAndProperties, this will be created at least twice.
Author
There was a problem hiding this comment.
Yes, you are right, I moved dictionary creation to GetFieldsAndProperties.
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.
This is an AI-assisted optimization, and actually a better version of #1021. If you like this change, the other one can be declined and closed.
The documentation below is AI generated.
Summary
DropHiddenMembersrepeatedly scans current members withFirstfor each matching source name. On development it also repeatedly enumerates the lazy name intersection. This change indexes the first current member for each name once per result enumeration and performs dictionary lookups while yielding source members in their original order.The dictionary uses ordinal, case-sensitive names and retains first-member precedence for duplicate names. It stores MemberInfo rather than eagerly reading MetadataToken; the existing token comparison stays unchanged. This removes the redundant intersection and gives expected O(current members + source members) filtering work for stable reflection collections, excluding reflection-property costs and hash collisions. No public API, package, configuration, or attribute-caching changes are included.
Relationship to existing PRs
This supersedes the HashSet/intersection approach in the still-open #1021. It is intended as the replacement implementation for that contribution, targeting development, rather than an overlapping second PR. The separate attribute-caching work in #1022 is not required or included.
Enumeration behavior and trade-offs
Initialization remains deferred until MoveNext and the lookup is rebuilt for every enumeration. The source is visited once; partial enumeration stops without scanning its remainder. All current members are indexed up front, including names absent from the source. This can cost more for empty sources or early termination. Active mutation, stateful sequences, custom reflection getters, null members/names, and exception timing are not claimed to be identical. The internal callers supply stable reflection arrays (with indexer filtering for properties).
Regression coverage
Fourteen focused MSTest/Shouldly tests cover hiding across properties and fields, private hiding, first-name precedence, case sensitivity, ordering/duplicates, empty inputs, re-enumeration, Adapt/MapToTarget, deferred execution, enumeration counts, bounded Name access, and unmatched token access. They call the internal helper directly. On isolated upstream and HashSet snapshots, ten semantic tests pass and the four operation-count tests fail; the dictionary passes all fourteen. For 64 members the Name-access test observed 12,480 accesses upstream and 4,416 with HashSet against a linear bound of 512.
Validation on Windows
dotnet test src/Mapster.Tests/Mapster.Tests.csproj --no-restore -c Release --filter FullyQualifiedName~WhenLookingUpHiddenMembersdotnet test src/Mapster.Tests/Mapster.Tests.csproj --no-build --no-restore -c Releasedotnet build src/Mapster/Mapster.csproj --no-restore -c Release -f netstandard2.0dotnet build src/Mapster.sln --no-restore -c Releasedotnet test src/Mapster.sln --no-build --no-restore -c Releasegit diff --checkPerformance measurements
Standalone measurements used the actual production assemblies: development
4a8aaa415d434ff86b0182617ad2062ab1794a19, that same base with #1021's HashSet method, and the dictionary patch. Attribute caching was absent from all variants. A local diagnostic harness (outside the patch) used Release/.NET 10.0.12 on Windows x64/i9-13900K, tiered compilation disabled equally, six fresh processes per variant in all six balanced order permutations, and three timed batches after warmup per scenario. Values below are medians of process means; allocations are managed bytes on the calling thread.Limitations
These measurements demonstrate an empty-source regression and increased early-stop allocations relative to upstream. Early-stop 256 timings were noisy (upstream process means 5.02-17.74 us, dictionary 6.37-6.90 us), so its median is not a reliable general speedup claim. Fresh compilation ranged 23.90-62.04 ms upstream, 9.07-9.32 ms HashSet, and 7.80-8.42 ms dictionary. The harness also measured no-overlap, mostly-nonmatching, empty-current, and mixed inherited property/field cases. It reused generated model types but created a fresh configuration and compiled both Map and MapToTarget on every compilation operation. This is a synthetic, single-machine/runtime diagnostic comparison, not a universal performance claim or a BenchmarkDotNet confidence-interval result. The eager-indexing trade-off warrants review; no pre-sizing, empty-source special case, or additional tuning is included.