Avoid allocations in GetHashCode implementations#13475
Conversation
Don't call ToLowerInvariant() or ToUpperInvariant() in GetHashCode implementations to produce a case-insensitive hash. Instead, use StringComparer.OrdinalIgnoreCase.GetHashCode(...) to avoid allocating strings in a potentially hot paths.
baronfel
left a comment
There was a problem hiding this comment.
Is there some analyzer for this that we could use? "Prefer GetHashCode from Comparer instead of string instance"?
There was a problem hiding this comment.
Pull request overview
Updates GetHashCode implementations to avoid allocating temporary strings for case-insensitive hashing, which is beneficial in potentially hot paths and better aligns hashing with the existing OrdinalIgnoreCase equality semantics.
Changes:
- Replace
ToLowerInvariant()/ToUpperInvariant()-based hashing withStringComparer.OrdinalIgnoreCase.GetHashCode(...). - Minor refactor of
SdkReference.GetHashCodecontrol flow while preserving case-insensitive behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/Utilities/TargetPlatformSDK.cs | Switches to StringComparer.OrdinalIgnoreCase in GetHashCode to avoid allocations. |
| src/Framework/Sdk/SdkReference.cs | Uses StringComparer.OrdinalIgnoreCase for hashing of Name/Version/MinimumVersion to avoid allocations. |
I don't know if there's an existing analyzer. However, it's fine to call |
…r null fields Agent-Logs-Url: https://github.com/dotnet/msbuild/sessions/42156923-5ae2-43c5-ada9-9909e3566afc Co-authored-by: DustinCampbell <116161+DustinCampbell@users.noreply.github.com>
rainersigwald
left a comment
There was a problem hiding this comment.
Had me scared for a second here, but fortunately these are both low-frequency uses, not our hottest hash path. Still worth fixing!
Two simple tweaks to stop calling
ToLowerInvariant()andToUpperInvariant()inGetHashCodeimplementations in order to produce a case-insensitive hash. Instead, useStringComparer.OrdinalIgnoreCase.GetHashCode(...)to avoid allocating strings in a potentially hot path.Context
Calling
ToLowerInvariant()orToUpperInvariant()insideGetHashCodeallocates a temporary string just to compute a hash — an unnecessary allocation in a potentially hot path. UsingStringComparer.OrdinalIgnoreCase.GetHashCode(...)produces the same case-insensitive result without any allocation.Changes Made
ToLowerInvariant()/ToUpperInvariant()-based hashing withStringComparer.OrdinalIgnoreCase.GetHashCode(...)inTargetPlatformSDK.GetHashCodeandSdkReference.GetHashCode.SdkReference.GetHashCodeto always apply the(hashCode * 397)position multiplier for every field (treating null as 0), preserving the original combining behavior and preventing hash collisions between different null patterns (e.g.{Name, null, "bar"}vs{Name, "bar", null}).Testing
SdkReferenceunit tests inMicrosoft.Build.Framework.UnitTestspass.Notes
The
SdkReference.GetHashCodefix ensures the hash-combining pattern is positionally stable: skipping the* 397multiply when a field is null would cause different null arrangements to produce identical hashes.