diff --git a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CachingFilePathComparer.cs b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CachingFilePathComparer.cs index 20fe46870c34c..737997600fd97 100644 --- a/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CachingFilePathComparer.cs +++ b/src/Workspaces/Core/Portable/Workspace/Solution/SolutionState.CachingFilePathComparer.cs @@ -34,13 +34,12 @@ private sealed class CachingFilePathComparer : IEqualityComparer public static readonly CachingFilePathComparer Instance = new(); /// - /// Lock to protect the the last string and hash code we computed. `enableThreadOwnerTracking: false` as we - /// don't need that tracking, and it substantially speeds up the spin lock (removing 0.7% cpu from solution load - /// scenario). + /// ThreadStatic so that gets its own copy it can safely read/write from, removing the need for expensive + /// contentious locks. The purpose of this type is to allow lookup of the same key across N dictionaries + /// efficiently from the same thread. So this accomplishes that purpose. /// - private SpinLock _lock = new(enableThreadOwnerTracking: false); - private string? _lastString; - private int _lastHashCode; + [ThreadStatic] + private static (string? lastString, int lastHashCode) s_data; private CachingFilePathComparer() { @@ -51,61 +50,19 @@ public bool Equals(string? x, string? y) public int GetHashCode([DisallowNull] string obj) { - if (TryGetCachedHashCode(obj, out var hashCode)) - return hashCode; + // SToub thinks this may be faster on NetFx as it will help the runtime with reading/writing from a single location. + ref var data = ref s_data; + if (ReferenceEquals(data.lastString, obj)) + return data.lastHashCode; // Hashing a different string than last time. Compute the hash and cache the value. // Specialized impl of OrdinalIgnoreCase.GetHashCode that is faster for the common case of an all-ASCII // string. Falls back to normal OrdinalIgnoreCase.GetHashCode for the uncommon case. - hashCode = GetNonRandomizedHashCodeOrdinalIgnoreCase(obj); + var hash = GetNonRandomizedHashCodeOrdinalIgnoreCase(obj); - var lockTaken = false; - try - { - _lock.Enter(ref lockTaken); - _lastString = obj; - _lastHashCode = hashCode; - } - finally - { - if (lockTaken) - _lock.Exit(); - } - - return hashCode; - } - - private bool TryGetCachedHashCode(string obj, out int hashCode) - { - var lastString = _lastString; - - // Quickly check if this is definitely *not* trying to hash the same string that this comparer was just used - // to hash. If that's the case, we can avoid taking the lock and just return false immediately. For the - // case when a lot of distinct strings are being hashed (say, when a dictionary is being populated), this - // means we only spin-wait once. - if (!ReferenceEquals(lastString, obj)) - { - hashCode = default; - return false; - } - - // Otherwise, take the lock, and now copy both the string and hash out. - var lockTaken = false; - try - { - _lock.Enter(ref lockTaken); - lastString = _lastString; - hashCode = _lastHashCode; - } - finally - { - if (lockTaken) - _lock.Exit(); - } - - // Check again, as another thread may have written into this field between the first check and taking the lock. - return ReferenceEquals(lastString, obj); + data = (obj, hash); + return hash; } // From https://github.com/dotnet/runtime/blob/5aa9687e110faa19d1165ba680e52585a822464d/src/libraries/System.Private.CoreLib/src/System/String.Comparison.cs#L921