Skip to content

Avoid allocations in GetHashCode implementations#13475

Merged
DustinCampbell merged 2 commits into
mainfrom
dev/dustinca/avoid-gethashcode-allocations
Apr 7, 2026
Merged

Avoid allocations in GetHashCode implementations#13475
DustinCampbell merged 2 commits into
mainfrom
dev/dustinca/avoid-gethashcode-allocations

Conversation

@DustinCampbell

@DustinCampbell DustinCampbell commented Mar 31, 2026

Copy link
Copy Markdown
Member

Two simple tweaks to stop calling ToLowerInvariant() and ToUpperInvariant() in GetHashCode implementations in order to produce a case-insensitive hash. Instead, use StringComparer.OrdinalIgnoreCase.GetHashCode(...) to avoid allocating strings in a potentially hot path.

Context

Calling ToLowerInvariant() or ToUpperInvariant() inside GetHashCode allocates a temporary string just to compute a hash — an unnecessary allocation in a potentially hot path. Using StringComparer.OrdinalIgnoreCase.GetHashCode(...) produces the same case-insensitive result without any allocation.

Changes Made

  • Replace ToLowerInvariant()/ToUpperInvariant()-based hashing with StringComparer.OrdinalIgnoreCase.GetHashCode(...) in TargetPlatformSDK.GetHashCode and SdkReference.GetHashCode.
  • Fix SdkReference.GetHashCode to 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

  • Existing SdkReference unit tests in Microsoft.Build.Framework.UnitTests pass.

Notes

The SdkReference.GetHashCode fix ensures the hash-combining pattern is positionally stable: skipping the * 397 multiply when a field is null would cause different null arrangements to produce identical hashes.

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.
Copilot AI review requested due to automatic review settings March 31, 2026 18:56

@baronfel baronfel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some analyzer for this that we could use? "Prefer GetHashCode from Comparer instead of string instance"?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 with StringComparer.OrdinalIgnoreCase.GetHashCode(...).
  • Minor refactor of SdkReference.GetHashCode control 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.

Comment thread src/Framework/Sdk/SdkReference.cs Outdated
@DustinCampbell

Copy link
Copy Markdown
Member Author

Is there some analyzer for this that we could use? "Prefer GetHashCode from Comparer instead of string instance"?

I don't know if there's an existing analyzer. However, it's fine to call GetHashCode on a string instance. The issue is with allocating a new throwaway string in order to call GetHashCode.

…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 rainersigwald left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had me scared for a second here, but fortunately these are both low-frequency uses, not our hottest hash path. Still worth fixing!

@DustinCampbell DustinCampbell merged commit 20e99d4 into main Apr 7, 2026
10 checks passed
@DustinCampbell DustinCampbell deleted the dev/dustinca/avoid-gethashcode-allocations branch April 7, 2026 23:12
This was referenced Jun 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants