diff --git a/src/Build/Microsoft.Build.csproj b/src/Build/Microsoft.Build.csproj index 1f773903ed9..a554ecfd951 100644 --- a/src/Build/Microsoft.Build.csproj +++ b/src/Build/Microsoft.Build.csproj @@ -357,10 +357,6 @@ - - - - Collections\CopyOnWriteDictionary.cs diff --git a/src/MSBuild/MSBuild.csproj b/src/MSBuild/MSBuild.csproj index a75bbeff256..98f08298b0b 100644 --- a/src/MSBuild/MSBuild.csproj +++ b/src/MSBuild/MSBuild.csproj @@ -133,10 +133,6 @@ - - - - diff --git a/src/MSBuildTaskHost/MSBuildTaskHost.csproj b/src/MSBuildTaskHost/MSBuildTaskHost.csproj index 901c6a53a8e..0437fdcbb36 100644 --- a/src/MSBuildTaskHost/MSBuildTaskHost.csproj +++ b/src/MSBuildTaskHost/MSBuildTaskHost.csproj @@ -129,15 +129,6 @@ NodeShutdown.cs - - IInternable.cs - - - WeakStringCache.cs - - - OpportunisticIntern.cs - ReadOnlyEmptyCollection.cs @@ -169,7 +160,6 @@ - LogMessagePacket.cs diff --git a/src/MSBuildTaskHost/WeakStringCache.Locking.cs b/src/MSBuildTaskHost/WeakStringCache.Locking.cs deleted file mode 100644 index d26dd984963..00000000000 --- a/src/MSBuildTaskHost/WeakStringCache.Locking.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.Build -{ - /// - /// Implements the WeakStringCache functionality on .NET Framework 3.5 where ConcurrentDictionary is not available. - /// - internal sealed partial class WeakStringCache : IDisposable - { - private readonly Dictionary _stringsByHashCode; - - public WeakStringCache() - { - _stringsByHashCode = new Dictionary(_initialCapacity); - } - - /// - /// Main entrypoint of this cache. Tries to look up a string that matches the given internable. If it succeeds, returns - /// the string and sets cacheHit to true. If the string is not found, calls ExpensiveConvertToString on the internable, - /// adds the resulting string to the cache, and returns it, setting cacheHit to false. - /// - /// The internable describing the string we're looking for. - /// A string matching the given internable. - public string GetOrCreateEntry(T internable, out bool cacheHit) where T : IInternable - { - int hashCode = GetInternableHashCode(internable); - - StringWeakHandle handle; - string result; - bool addingNewHandle = false; - - lock (_stringsByHashCode) - { - if (_stringsByHashCode.TryGetValue(hashCode, out handle)) - { - result = handle.GetString(internable); - if (result != null) - { - cacheHit = true; - return result; - } - } - else - { - handle = new StringWeakHandle(); - addingNewHandle = true; - } - - // We don't have the string in the cache - create it. - result = internable.ExpensiveConvertToString(); - - // Set the handle to reference the new string. - handle.SetString(result); - - if (addingNewHandle) - { - // Prevent the dictionary from growing forever with GC handles that don't reference live strings anymore. - if (_stringsByHashCode.Count >= _scavengeThreshold) - { - // Get rid of unused handles. - ScavengeNoLock(); - // And do this again when the number of handles reaches double the current after-scavenge number. - _scavengeThreshold = _stringsByHashCode.Count * 2; - } - } - _stringsByHashCode[hashCode] = handle; - } - - cacheHit = false; - return result; - } - - /// - /// Iterates over the cache and removes unused GC handles, i.e. handles that don't reference live strings. - /// This is expensive so try to call such that the cost is amortized to O(1) per GetOrCreateEntry() invocation. - /// Assumes the lock is taken by the caller. - /// - private void ScavengeNoLock() - { - List keysToRemove = null; - foreach (KeyValuePair entry in _stringsByHashCode) - { - if (!entry.Value.IsUsed) - { - entry.Value.Free(); - keysToRemove ??= new List(); - keysToRemove.Add(entry.Key); - } - } - if (keysToRemove != null) - { - for (int i = 0; i < keysToRemove.Count; i++) - { - _stringsByHashCode.Remove(keysToRemove[i]); - } - } - } - - /// - /// Public version of ScavengeUnderLock() which takes the lock. - /// - public void Scavenge() - { - lock (_stringsByHashCode) - { - ScavengeNoLock(); - } - } - - /// - /// Returns internal debug counters calculated based on the current state of the cache. - /// - public DebugInfo GetDebugInfo() - { - lock (_stringsByHashCode) - { - return GetDebugInfoImpl(); - } - } - } -} diff --git a/src/Shared/Constants.cs b/src/Shared/Constants.cs index 42f82771737..d8b2c66c98d 100644 --- a/src/Shared/Constants.cs +++ b/src/Shared/Constants.cs @@ -60,8 +60,6 @@ internal static class MSBuildConstants /// internal const string CurrentToolsVersion = "Current"; - // if you change the key also change the following clones - // Microsoft.Build.OpportunisticIntern.BucketedPrioritizedStringList.TryIntern internal const string MSBuildDummyGlobalPropertyHeader = "MSBuildProjectInstance"; /// diff --git a/src/Shared/IInternable.cs b/src/Shared/IInternable.cs deleted file mode 100644 index 9bfa741e097..00000000000 --- a/src/Shared/IInternable.cs +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Text; -using Microsoft.Build.Shared; - -namespace Microsoft.Build -{ - #region IInternable - /// - /// Define the methods needed to intern something. - /// - internal interface IInternable - { - /// - /// The length of the target. - /// - int Length { get; } - - /// - /// Indexer into the target. Presumed to be fast. - /// - char this[int index] { get; } - - /// - /// Convert target to string. Presumed to be slow (and will be called just once). - /// - string ExpensiveConvertToString(); - - /// - /// Compare target to string. Assumes string is of equal or smaller length than target. - /// - bool StartsWithStringByOrdinalComparison(string other); - - /// - /// Reference compare target to string. If target is non-string this should return false. - /// - bool ReferenceEquals(string other); - } - #endregion - - - #region IInternable Implementations - /// - /// A wrapper over StringBuilder. - /// - internal readonly struct StringBuilderInternTarget : IInternable - { - /// - /// The held StringBuilder - /// - private readonly StringBuilder _target; - - /// - /// Pointless comment about constructor. - /// - internal StringBuilderInternTarget(StringBuilder target) - { - _target = target; - } - - /// - /// The length of the target. - /// - public int Length => _target.Length; - - /// - /// Indexer into the target. Presumed to be fast. - /// - public char this[int index] => _target[index]; - - /// - /// Never reference equals to string. - /// - public bool ReferenceEquals(string other) => false; - - /// - /// Convert target to string. Presumed to be slow (and will be called just once). - /// - public string ExpensiveConvertToString() - { - // PERF NOTE: This will be an allocation hot-spot because the StringBuilder is finally determined to - // not be internable. There is still only one conversion of StringBuilder into string it has just - // moved into this single spot. - return _target.ToString(); - } - - /// - /// Compare target to string. Assumes string is of equal or smaller length than target. - /// - public bool StartsWithStringByOrdinalComparison(string other) - { -#if DEBUG - ErrorUtilities.VerifyThrow(other.Length <= _target.Length, "should be at most as long as target"); -#endif - int length = other.Length; - - // Backwards because the end of the string is more likely to be different earlier in the loop. - // For example, C:\project1, C:\project2 - for (int i = length - 1; i >= 0; --i) - { - if (_target[i] != other[i]) - { - return false; - } - } - - return true; - } - - /// - /// Don't use this function. Use ExpensiveConvertToString - /// - public override string ToString() => throw new InvalidOperationException(); - } - - /// - /// A wrapper over char[]. - /// - internal readonly struct CharArrayInternTarget : IInternable - { - /// - /// Start index for the string - /// - private readonly int _startIndex; - - /// - /// The held array - /// - private readonly char[] _target; - - /// - /// Pointless comment about constructor. - /// - internal CharArrayInternTarget(char[] target, int count) - : this(target, 0, count) - { - } - - /// - /// Pointless comment about constructor. - /// - internal CharArrayInternTarget(char[] target, int startIndex, int count) - { -#if DEBUG - if (startIndex + count > target.Length) - { - ErrorUtilities.ThrowInternalError("wrong length"); - } -#endif - _target = target; - _startIndex = startIndex; - Length = count; - } - - /// - /// The length of the target. - /// - public int Length { get; } - - /// - /// Indexer into the target. Presumed to be fast. - /// - public char this[int index] - { - get - { - return _target[index + _startIndex]; - } - } - - /// - /// Convert target to string. Presumed to be slow (and will be called just once). - /// - public bool ReferenceEquals(string other) - { - return false; - } - - /// - /// Convert target to string. Presumed to be slow (and will be called just once). - /// - public string ExpensiveConvertToString() - { - // PERF NOTE: This will be an allocation hot-spot because the char[] is finally determined to - // not be internable. There is still only one conversion of char[] into string it has just - // moved into this single spot. - return new string(_target, _startIndex, Length); - } - - /// - /// Compare target to string. Assumes string is of equal or smaller length than target. - /// - public bool StartsWithStringByOrdinalComparison(string other) - { -#if DEBUG - ErrorUtilities.VerifyThrow(other.Length <= Length, "should be at most as long as target"); -#endif - // Backwards because the end of the string is (by observation of Australian Government build) more likely to be different earlier in the loop. - // For example, C:\project1, C:\project2 - for (int i = other.Length - 1; i >= 0; --i) - { - if (_target[i + _startIndex] != other[i]) - { - return false; - } - } - - return true; - } - - /// - /// Don't use this function. Use ExpensiveConvertToString - /// - public override string ToString() - { - throw new InvalidOperationException(); - } - } - - /// - /// Wrapper over a string. - /// - internal readonly struct StringInternTarget : IInternable - { - /// - /// Stores the wrapped string. - /// - private readonly string _target; - - /// - /// Constructor of the class - /// - /// The string to wrap - internal StringInternTarget(string target) - { - ErrorUtilities.VerifyThrowArgumentLength(target, nameof(target)); - _target = target; - } - - /// - /// Gets the length of the target string. - /// - public int Length => _target.Length; - - /// - /// Gets the n character in the target string. - /// - /// Index of the character to gather. - /// The character in the position marked by index. - public char this[int index] => _target[index]; - - /// - /// Returns the target which is already a string. - /// - /// The target string. - public string ExpensiveConvertToString() => _target; - - /// - /// Compare target to string. Assumes string is of equal or smaller length than target. - /// - /// The string to compare with the target. - /// True if target starts with , false otherwise. - public bool StartsWithStringByOrdinalComparison(string other) => _target.StartsWith(other, StringComparison.Ordinal); - - /// - /// Verifies if the reference of the target string is the same of the given string. - /// - /// The string reference to compare to. - /// True if both references are equal, false otherwise. - public bool ReferenceEquals(string other) => ReferenceEquals(_target, other); - } - - /// - /// Wrapper over a substring of a string. - /// - internal readonly struct SubstringInternTarget : IInternable - { - /// - /// Stores the wrapped string. - /// - private readonly string _target; - - /// - /// Start index of the substring within the wrapped string. - /// - private readonly int _startIndex; - - /// - /// Constructor of the class - /// - /// The string to wrap. - /// Start index of the substring within . - /// Length of the substring. - internal SubstringInternTarget(string target, int startIndex, int length) - { -#if DEBUG - if (startIndex + length > target.Length) - { - ErrorUtilities.ThrowInternalError("wrong length"); - } -#endif - _target = target; - _startIndex = startIndex; - Length = length; - } - - /// - /// Gets the length of the target substring. - /// - public int Length { get; } - - /// - /// Gets the n character in the target substring. - /// - /// Index of the character to gather. - /// The character in the position marked by index. - public char this[int index] => _target[index + _startIndex]; - - /// - /// Returns the target substring as a string. - /// - /// The substring. - public string ExpensiveConvertToString() => _target.Substring(_startIndex, Length); - - /// - /// Compare target substring to a string. Assumes string is of equal or smaller length than the target substring. - /// - /// The string to compare with the target substring. - /// True if target substring starts with , false otherwise. - public bool StartsWithStringByOrdinalComparison(string other) => String.CompareOrdinal(_target, _startIndex, other, 0, other.Length) == 0; - - /// - /// Never reference equals to string. - /// - public bool ReferenceEquals(string other) => false; - } - - #endregion -} diff --git a/src/Shared/OpportunisticIntern.cs b/src/Shared/OpportunisticIntern.cs deleted file mode 100644 index 48d5d407c7c..00000000000 --- a/src/Shared/OpportunisticIntern.cs +++ /dev/null @@ -1,1093 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -#if !CLR2COMPATIBILITY -using System.Collections.Concurrent; -#endif -using System.Text; -using System.Linq; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using Microsoft.Build.Shared; -using Microsoft.Build.Utilities; - -namespace Microsoft.Build -{ - /// - /// This class is used to selectively intern strings. It should be used at the point of new string creation. - /// For example, - /// - /// string interned = OpportunisticIntern.Intern(String.Join(",",someStrings)); - /// - /// There are currently two underlying implementations. The new default one in WeakStringCacheInterner is based on weak GC handles. - /// The legacy one in BucketedPrioritizedStringList is available only as an escape hatch by setting an environment variable. - /// - /// The legacy implementation uses heuristics to decide whether it will be efficient to intern a string or not. There is no - /// guarantee that a string will intern. - /// - /// The thresholds and sizes were determined by experimentation to give the best number of bytes saved - /// at reasonable elapsed time cost. - /// - /// The new implementation interns all strings but maintains only weak references so it doesn't keep the strings alive. - /// - internal sealed class OpportunisticIntern - { - /// - /// Defines the interner interface as we currently implement more than one. - /// - private interface IInternerImplementation - { - /// - /// Converts the given internable candidate to its string representation. Efficient implementions have side-effects - /// of caching the results to end up with as few duplicates on the managed heap as practical. - /// - string InterningToString(T candidate) where T : IInternable; - - /// - /// Prints implementation specific interning statistics to the console. - /// - /// A string identifying the interner in the output. - void ReportStatistics(string heading); - } - - /// - /// The singleton instance of OpportunisticIntern. - /// - private static OpportunisticIntern _instance = new OpportunisticIntern(); - internal static OpportunisticIntern Instance => _instance; - - private readonly bool _useLegacyInterner = Traits.Instance.UseLegacyStringInterner; - private readonly bool _useSimpleConcurrency = Traits.Instance.UseSimpleInternConcurrency; - - /// - /// The size of the small mru list. - /// - private readonly int _smallMruSize; - - /// - /// The size of the large mru list. - /// - private readonly int _largeMruSize; - - /// - /// The size of the huge mru list. - /// - private readonly int _hugeMruSize; - - /// - /// The smallest size a string can be to be considered small. - /// - private readonly int _smallMruThreshold; - - /// - /// The smallest size a string can be to be considered large. - /// - private readonly int _largeMruThreshold; - - /// - /// The smallest size a string can be to be considered huge. - /// - private readonly int _hugeMruThreshold; - - /// - /// The smallest size a string can be to be ginormous. - /// 8K for large object heap. - /// - private readonly int _ginormousThreshold; - - /// - /// The interner implementation in use. - /// - private IInternerImplementation _interner; - - #region Statistics - /// - /// What if Mru lists were infinitely long? - /// - private BucketedPrioritizedStringList _whatIfInfinite; - - /// - /// What if we doubled the size of the Mru lists? - /// - private BucketedPrioritizedStringList _whatIfDoubled; - - /// - /// What if we halved the size of the Mru lists? - /// - private BucketedPrioritizedStringList _whatIfHalved; - - /// - /// What if the size of Mru lists was zero? (We still intern tiny strings in this case) - /// - private BucketedPrioritizedStringList _whatIfZero; - #endregion - - private OpportunisticIntern() - { - _smallMruSize = AssignViaEnvironment("MSBUILDSMALLINTERNSIZE", 50); - _largeMruSize = AssignViaEnvironment("MSBUILDLARGEINTERNSIZE", 100); - _hugeMruSize = AssignViaEnvironment("MSBUILDHUGEINTERNSIZE", 100); - _smallMruThreshold = AssignViaEnvironment("MSBUILDSMALLINTERNTHRESHOLD", 50); - _largeMruThreshold = AssignViaEnvironment("MSBUILDLARGEINTERNTHRESHOLD", 70); - _hugeMruThreshold = AssignViaEnvironment("MSBUILDHUGEINTERNTHRESHOLD", 200); - _ginormousThreshold = AssignViaEnvironment("MSBUILDGINORMOUSINTERNTHRESHOLD", 8000); - - _interner = _useLegacyInterner - ? (IInternerImplementation)new BucketedPrioritizedStringList(gatherStatistics: false, _smallMruSize, _largeMruSize, _hugeMruSize, - _smallMruThreshold, _largeMruThreshold, _hugeMruThreshold, _ginormousThreshold, _useSimpleConcurrency) - : (IInternerImplementation)new WeakStringCacheInterner(gatherStatistics: false); - } - - /// - /// Recreates the singleton instance based on the current environment (test only). - /// - internal static void ResetForTests() - { - Debug.Assert(BuildEnvironmentHelper.Instance.RunningTests); - _instance = new OpportunisticIntern(); - } - - /// - /// Assign an int from an environment variable. If its not present, use the default. - /// - private int AssignViaEnvironment(string env, int @default) - { - string threshold = Environment.GetEnvironmentVariable(env); - if (!string.IsNullOrEmpty(threshold)) - { - if (int.TryParse(threshold, out int result)) - { - return result; - } - } - - return @default; - } - - /// - /// Turn on statistics gathering. - /// - internal void EnableStatisticsGathering() - { - if (_useLegacyInterner) - { - // Statistics include several 'what if' scenarios such as doubling the size of the MRU lists. - _interner = new BucketedPrioritizedStringList(gatherStatistics: true, _smallMruSize, _largeMruSize, _hugeMruSize, _smallMruThreshold, _largeMruThreshold, _hugeMruThreshold, _ginormousThreshold, _useSimpleConcurrency); - _whatIfInfinite = new BucketedPrioritizedStringList(gatherStatistics: true, int.MaxValue, int.MaxValue, int.MaxValue, _smallMruThreshold, _largeMruThreshold, _hugeMruThreshold, _ginormousThreshold, _useSimpleConcurrency); - _whatIfDoubled = new BucketedPrioritizedStringList(gatherStatistics: true, _smallMruSize * 2, _largeMruSize * 2, _hugeMruSize * 2, _smallMruThreshold, _largeMruThreshold, _hugeMruThreshold, _ginormousThreshold, _useSimpleConcurrency); - _whatIfHalved = new BucketedPrioritizedStringList(gatherStatistics: true, _smallMruSize / 2, _largeMruSize / 2, _hugeMruSize / 2, _smallMruThreshold, _largeMruThreshold, _hugeMruThreshold, _ginormousThreshold, _useSimpleConcurrency); - _whatIfZero = new BucketedPrioritizedStringList(gatherStatistics: true, 0, 0, 0, _smallMruThreshold, _largeMruThreshold, _hugeMruThreshold, _ginormousThreshold, _useSimpleConcurrency); - } - else - { - _interner = new WeakStringCacheInterner(gatherStatistics: true); - } - } - - /// - /// Intern the given internable. - /// - internal static string InternableToString(T candidate) where T : IInternable - { - return Instance.InternableToStringImpl(candidate); - } - - /// - /// Potentially Intern the given string builder. - /// - internal static string StringBuilderToString(StringBuilder candidate) - { - return Instance.InternableToStringImpl(new StringBuilderInternTarget(candidate)); - } - - /// - /// Potentially Intern the given char array. - /// - internal static string CharArrayToString(char[] candidate, int count) - { - return Instance.InternableToStringImpl(new CharArrayInternTarget(candidate, count)); - } - - /// - /// Potentially Intern the given char array. - /// - internal static string CharArrayToString(char[] candidate, int startIndex, int count) - { - return Instance.InternableToStringImpl(new CharArrayInternTarget(candidate, startIndex, count)); - } - - /// - /// Potentially Intern the given string. - /// - /// The string to intern. - /// The interned string, or the same string if it could not be interned. - internal static string InternStringIfPossible(string candidate) - { - return Instance.InternableToStringImpl(new StringInternTarget(candidate)); - } - - /// - /// Intern the given internable. - /// - private string InternableToStringImpl(T candidate) where T : IInternable - { - if (candidate.Length == 0) - { - // As in the case that a property or itemlist has evaluated to empty. - return string.Empty; - } - - if (_whatIfInfinite != null) - { - _whatIfInfinite.InterningToString(candidate); - _whatIfDoubled.InterningToString(candidate); - _whatIfHalved.InterningToString(candidate); - _whatIfZero.InterningToString(candidate); - } - - string result = _interner.InterningToString(candidate); -#if DEBUG - string expected = candidate.ExpensiveConvertToString(); - if (!String.Equals(result, expected)) - { - ErrorUtilities.ThrowInternalError("Interned string {0} should have been {1}", result, expected); - } -#endif - return result; - } - - /// - /// Report statistics about interning. Don't call unless GatherStatistics has been called beforehand. - /// - internal void ReportStatistics() - { - _interner.ReportStatistics("Main"); - if (_useLegacyInterner) - { - _whatIfInfinite.ReportStatistics("if Infinite"); - _whatIfDoubled.ReportStatistics("if Doubled"); - _whatIfHalved.ReportStatistics("if Halved"); - _whatIfZero.ReportStatistics("if Zero"); - Console.WriteLine(" * Even for MRU size of zero there will still be some intern hits because of the tiny "); - Console.WriteLine(" string matching (eg. 'true')"); - } - } - - private static bool TryInternHardcodedString(T candidate, string str, ref string interned) where T : IInternable - { - Debug.Assert(candidate.Length == str.Length); - - if (candidate.StartsWithStringByOrdinalComparison(str)) - { - interned = str; - return true; - } - return false; - } - - /// - /// Try to match the candidate with small number of hardcoded interned string literals. - /// The return value indicates how the string was interned (if at all). - /// - /// - /// True if the candidate matched a hardcoded literal, null if it matched a "do not intern" string, false otherwise. - /// - private static bool? TryMatchHardcodedStrings(T candidate, out string interned) where T : IInternable - { - int length = candidate.Length; - interned = null; - - // Each of the hard-coded small strings below showed up in a profile run with considerable duplication in memory. - if (length == 2) - { - if (candidate[1] == '#') - { - if (candidate[0] == 'C') - { - interned = "C#"; - return true; - } - - if (candidate[0] == 'F') - { - interned = "F#"; - return true; - } - } - - if (candidate[0] == 'V' && candidate[1] == 'B') - { - interned = "VB"; - return true; - } - } - else if (length == 4) - { - if (TryInternHardcodedString(candidate, "TRUE", ref interned) || - TryInternHardcodedString(candidate, "True", ref interned) || - TryInternHardcodedString(candidate, "Copy", ref interned) || - TryInternHardcodedString(candidate, "true", ref interned) || - TryInternHardcodedString(candidate, "v4.0", ref interned)) - { - return true; - } - } - else if (length == 5) - { - if (TryInternHardcodedString(candidate, "FALSE", ref interned) || - TryInternHardcodedString(candidate, "false", ref interned) || - TryInternHardcodedString(candidate, "Debug", ref interned) || - TryInternHardcodedString(candidate, "Build", ref interned) || - TryInternHardcodedString(candidate, "Win32", ref interned)) - { - return true; - } - } - else if (length == 6) - { - if (TryInternHardcodedString(candidate, "''!=''", ref interned) || - TryInternHardcodedString(candidate, "AnyCPU", ref interned)) - { - return true; - } - } - else if (length == 7) - { - if (TryInternHardcodedString(candidate, "Library", ref interned) || - TryInternHardcodedString(candidate, "MSBuild", ref interned) || - TryInternHardcodedString(candidate, "Release", ref interned)) - { - return true; - } - } - // see Microsoft.Build.BackEnd.BuildRequestConfiguration.CreateUniqueGlobalProperty - else if (length > MSBuildConstants.MSBuildDummyGlobalPropertyHeader.Length && - candidate.StartsWithStringByOrdinalComparison(MSBuildConstants.MSBuildDummyGlobalPropertyHeader)) - { - // don't want to leak unique strings into the cache - interned = candidate.ExpensiveConvertToString(); - return null; - } - else if (length == 24) - { - if (TryInternHardcodedString(candidate, "ResolveAssemblyReference", ref interned)) - { - return true; - } - } - return false; - } - - /// - /// Implements interning based on a WeakStringCache (new implementation). - /// - private class WeakStringCacheInterner : IInternerImplementation - { - /// - /// Enumerates the possible interning results. - /// - private enum InternResult - { - MatchedHardcodedString, - FoundInWeakStringCache, - AddedToWeakStringCache, - RejectedFromInterning - } - - /// - /// The cache to keep strings in. - /// - private readonly WeakStringCache _weakStringCache = new WeakStringCache(); - -#region Statistics - /// - /// Whether or not to gather statistics. - /// - private readonly bool _gatherStatistics; - - /// - /// Number of times interning with hardcoded string literals worked. - /// - private int _hardcodedInternHits; - - /// - /// Number of times the regular interning path found the string in the cache. - /// - private int _regularInternHits; - - /// - /// Number of times the regular interning path added the string to the cache. - /// - private int _regularInternMisses; - - /// - /// Number of times interning wasn't attempted. - /// - private int _rejectedStrings; - - /// - /// Total number of strings eliminated by interning. - /// - private int _internEliminatedStrings; - - /// - /// Total number of chars eliminated across all strings. - /// - private int _internEliminatedChars; - - /// - /// Maps strings that went though the regular (i.e. not hardcoded) interning path to the number of times they have been - /// seen. The higher the number the better the payoff if the string had been hardcoded. - /// - private Dictionary _missedHardcodedStrings; - -#endregion - - public WeakStringCacheInterner(bool gatherStatistics) - { - if (gatherStatistics) - { - _missedHardcodedStrings = new Dictionary(); - } - _gatherStatistics = gatherStatistics; - } - - /// - /// Intern the given internable. - /// - public string InterningToString(T candidate) where T : IInternable - { - if (_gatherStatistics) - { - return InternWithStatistics(candidate); - } - else - { - TryIntern(candidate, out string result); - return result; - } - } - - /// - /// Report statistics to the console. - /// - public void ReportStatistics(string heading) - { - string title = "Opportunistic Intern (" + heading + ")"; - Console.WriteLine("\n{0}{1}{0}", new string('=', 41 - (title.Length / 2)), title); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Hardcoded Hits", _hardcodedInternHits, "hits"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Hardcoded Rejects", _rejectedStrings, "rejects"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "WeakStringCache Hits", _regularInternHits, "hits"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "WeakStringCache Misses", _regularInternMisses, "misses"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Strings*", _internEliminatedStrings, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Chars", _internEliminatedChars, "chars"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Estimated Eliminated Bytes", _internEliminatedChars * 2, "bytes"); - Console.WriteLine("Elimination assumes that strings provided were unique objects."); - Console.WriteLine("|---------------------------------------------------------------------------------|"); - - IEnumerable topMissingHardcodedString = - _missedHardcodedStrings - .OrderByDescending(kv => kv.Value * kv.Key.Length) - .Take(15) - .Where(kv => kv.Value > 1) - .Select(kv => string.Format(CultureInfo.InvariantCulture, "({1} instances x each {2} chars)\n{0}", kv.Key, kv.Value, kv.Key.Length)); - - Console.WriteLine("##########Top Missing Hardcoded Strings: \n{0} ", string.Join("\n==============\n", topMissingHardcodedString.ToArray())); - Console.WriteLine(); - - WeakStringCache.DebugInfo debugInfo = _weakStringCache.GetDebugInfo(); - Console.WriteLine("WeakStringCache statistics:"); - Console.WriteLine("String count live/collected/total = {0}/{1}/{2}", debugInfo.LiveStringCount, debugInfo.CollectedStringCount, debugInfo.LiveStringCount + debugInfo.CollectedStringCount); - } - - /// - /// Try to intern the string. - /// The return value indicates the how the string was interned (if at all). - /// - private InternResult TryIntern(T candidate, out string interned) where T : IInternable - { - // First, try the hard coded intern strings. - bool? hardcodedMatchResult = TryMatchHardcodedStrings(candidate, out interned); - if (hardcodedMatchResult != false) - { - // Either matched a hardcoded string or is explicitly not to be interned. - return hardcodedMatchResult.HasValue ? InternResult.MatchedHardcodedString : InternResult.RejectedFromInterning; - } - - interned = _weakStringCache.GetOrCreateEntry(candidate, out bool cacheHit); - return cacheHit ? InternResult.FoundInWeakStringCache : InternResult.AddedToWeakStringCache; - } - - /// - /// Version of Intern that gathers statistics - /// - private string InternWithStatistics(T candidate) where T : IInternable - { - lock (_missedHardcodedStrings) - { - InternResult internResult = TryIntern(candidate, out string result); - - switch (internResult) - { - case InternResult.MatchedHardcodedString: - _hardcodedInternHits++; - break; - case InternResult.FoundInWeakStringCache: - _regularInternHits++; - break; - case InternResult.AddedToWeakStringCache: - _regularInternMisses++; - break; - case InternResult.RejectedFromInterning: - _rejectedStrings++; - break; - } - - if (internResult != InternResult.MatchedHardcodedString && internResult != InternResult.RejectedFromInterning) - { - _missedHardcodedStrings.TryGetValue(result, out int priorCount); - _missedHardcodedStrings[result] = priorCount + 1; - } - - if (!candidate.ReferenceEquals(result)) - { - // Reference changed so 'candidate' is now released and should save memory. - _internEliminatedStrings++; - _internEliminatedChars += candidate.Length; - } - - return result; - } - } - } - - /// - /// Manages a set of mru lists that hold strings in varying size ranges (legacy implementation). - /// - private class BucketedPrioritizedStringList : IInternerImplementation - { - /// - /// The small string Mru list. - /// - private readonly PrioritizedStringList _smallMru; - - /// - /// The large string Mru list. - /// - private readonly PrioritizedStringList _largeMru; - - /// - /// The huge string Mru list. - /// - private readonly PrioritizedStringList _hugeMru; - - /// - /// Three most recently used strings over 8K. - /// - private readonly LinkedList _ginormous = new LinkedList(); - - /// - /// The smallest size a string can be to be considered small. - /// - private readonly int _smallMruThreshold; - - /// - /// The smallest size a string can be to be considered large. - /// - private readonly int _largeMruThreshold; - - /// - /// The smallest size a string can be to be considered huge. - /// - private readonly int _hugeMruThreshold; - - /// - /// The smallest size a string can be to be ginormous. - /// - private readonly int _ginormousThreshold; - - private readonly bool _useSimpleConcurrency; - -#if !CLR2COMPATIBILITY - // ConcurrentDictionary starts with capacity 31 but we're usually adding far more than that. Make a better first capacity guess to reduce - // ConcurrentDictionary having to take all internal locks to upgrade its bucket list. Note that the number should be prime per the - // comments on the code at https://referencesource.microsoft.com/#mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,122 - // Also note default lock count is NativeMethodsShared.GetLogicalCoreCount() from the same code. - private const int InitialCapacity = 2053; - private readonly ConcurrentDictionary _internedStrings = new ConcurrentDictionary(NativeMethodsShared.GetLogicalCoreCount(), InitialCapacity, StringComparer.Ordinal); -#endif - -#region Statistics - /// - /// Whether or not to gather statistics - /// - private readonly bool _gatherStatistics; - - /// - /// Number of times interning worked. - /// - private int _internHits; - - /// - /// Number of times interning didn't work. - /// - private int _internMisses; - - /// - /// Number of times interning wasn't attempted. - /// - private int _internRejects; - - /// - /// Total number of strings eliminated by interning. - /// - private int _internEliminatedStrings; - - /// - /// Total number of chars eliminated across all strings. - /// - private int _internEliminatedChars; - - /// - /// Number of times the ginourmous string hit. - /// - private int _ginormousHits; - - /// - /// Number of times the ginourmous string missed. - /// - private int _ginormousMisses; - - /// - /// Chars interned for ginormous range. - /// - private int _ginormousCharsSaved; - - /// - /// Whether or not to track ginormous strings. - /// - private readonly bool _dontTrack; - - /// - /// The time spent interning. - /// - private readonly Stopwatch _stopwatch; - - /// - /// Strings which did not intern - /// - private readonly Dictionary _missedStrings; - - /// - /// Strings which we didn't attempt to intern - /// - private readonly Dictionary _rejectedStrings; - - /// - /// Number of ginormous strings to keep - /// By observation of Auto7, there are about three variations of the huge solution config blob - /// There aren't really any other strings of this size, but make it 10 to be sure. (There will barely be any misses) - /// - private const int GinormousSize = 10; - -#endregion - - /// - /// Construct. - /// - internal BucketedPrioritizedStringList(bool gatherStatistics, int smallMruSize, int largeMruSize, int hugeMruSize, int smallMruThreshold, int largeMruThreshold, int hugeMruThreshold, int ginormousThreshold, bool useSimpleConcurrency) - { - if (smallMruSize == 0 && largeMruSize == 0 && hugeMruSize == 0) - { - _dontTrack = true; - } - - _smallMru = new PrioritizedStringList(smallMruSize); - _largeMru = new PrioritizedStringList(largeMruSize); - _hugeMru = new PrioritizedStringList(hugeMruSize); - _smallMruThreshold = smallMruThreshold; - _largeMruThreshold = largeMruThreshold; - _hugeMruThreshold = hugeMruThreshold; - _ginormousThreshold = ginormousThreshold; - _useSimpleConcurrency = useSimpleConcurrency; - - for (int i = 0; i < GinormousSize; i++) - { - _ginormous.AddFirst(new WeakReference(string.Empty)); - } - - _gatherStatistics = gatherStatistics; - if (gatherStatistics) - { - _stopwatch = new Stopwatch(); - _missedStrings = new Dictionary(StringComparer.Ordinal); - _rejectedStrings = new Dictionary(StringComparer.Ordinal); - } - } - - /// - /// Intern the given internable. - /// - public string InterningToString(T candidate) where T : IInternable - { - if (_gatherStatistics) - { - return InternWithStatistics(candidate); - } - else - { - TryIntern(candidate, out string result); - return result; - } - } - - /// - /// Report statistics to the console. - /// - public void ReportStatistics(string heading) - { - string title = "Opportunistic Intern (" + heading + ")"; - Console.WriteLine("\n{0}{1}{0}", new string('=', 41 - (title.Length / 2)), title); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Intern Hits", _internHits, "hits"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Intern Misses", _internMisses, "misses"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Intern Rejects (as shorter than " + _smallMruThreshold + " bytes)", _internRejects, "rejects"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Strings*", _internEliminatedStrings, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Eliminated Chars", _internEliminatedChars, "chars"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Estimated Eliminated Bytes", _internEliminatedChars * 2, "bytes"); - Console.WriteLine("Elimination assumes that strings provided were unique objects."); - Console.WriteLine("|---------------------------------------------------------------------------------|"); - KeyValuePair held = _smallMru.Statistics(); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Small Strings MRU Size", Instance._smallMruSize, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Small Strings (>=" + _smallMruThreshold + " chars) Held", held.Key, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Small Estimated Bytes Held", held.Value * 2, "bytes"); - Console.WriteLine("|---------------------------------------------------------------------------------|"); - held = _largeMru.Statistics(); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Large Strings MRU Size", Instance._largeMruSize, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Large Strings (>=" + _largeMruThreshold + " chars) Held", held.Key, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Large Estimated Bytes Held", held.Value * 2, "bytes"); - Console.WriteLine("|---------------------------------------------------------------------------------|"); - held = _hugeMru.Statistics(); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Huge Strings MRU Size", Instance._hugeMruSize, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Huge Strings (>=" + _hugeMruThreshold + " chars) Held", held.Key, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Huge Estimated Bytes Held", held.Value * 2, "bytes"); - Console.WriteLine("|---------------------------------------------------------------------------------|"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous Strings MRU Size", GinormousSize, "strings"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous (>=" + _ginormousThreshold + " chars) Hits", _ginormousHits, "hits"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous Misses", _ginormousMisses, "misses"); - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Ginormous Chars Saved", _ginormousCharsSaved, "chars"); - Console.WriteLine("|---------------------------------------------------------------------------------|"); - - // There's no point in reporting the ginormous string because it will have evaporated by now. - Console.WriteLine("||{0,50}|{1,20:N0}|{2,8}|", "Time Spent Interning", _stopwatch.ElapsedMilliseconds, "ms"); - Console.WriteLine("{0}{0}", new string('=', 41)); - - IEnumerable topMissingString = - _missedStrings - .OrderByDescending(kv => kv.Value * kv.Key.Length) - .Take(15) - .Where(kv => kv.Value > 1) - .Select(kv => string.Format(CultureInfo.InvariantCulture, "({1} instances x each {2} chars = {3}KB wasted)\n{0}", kv.Key, kv.Value, kv.Key.Length, (kv.Value - 1) * kv.Key.Length * 2 / 1024)); - - Console.WriteLine("##########Top Missed Strings: \n{0} ", string.Join("\n==============\n", topMissingString.ToArray())); - Console.WriteLine(); - - IEnumerable topRejectedString = - _rejectedStrings - .OrderByDescending(kv => kv.Value * kv.Key.Length) - .Take(15) - .Where(kv => kv.Value > 1) - .Select(kv => string.Format(CultureInfo.InvariantCulture, "({1} instances x each {2} chars = {3}KB wasted)\n{0}", kv.Key, kv.Value, kv.Key.Length, (kv.Value - 1) * kv.Key.Length * 2 / 1024)); - - Console.WriteLine("##########Top Rejected Strings: \n{0} ", string.Join("\n==============\n", topRejectedString.ToArray())); - } - - /// - /// Try to intern the string. - /// Return true if an interned value could be returned. - /// Return false if it was added to the intern list, but wasn't there already. - /// Return null if it didn't meet the length criteria for any of the buckets. Interning was rejected - /// - private bool? TryIntern(T candidate, out string interned) where T : IInternable - { - int length = candidate.Length; - interned = null; - - // First, try the hard coded intern strings. - // Each of the hard-coded small strings below showed up in a profile run with considerable duplication in memory. - if (!_dontTrack) - { - bool? hardcodedMatchResult = TryMatchHardcodedStrings(candidate, out interned); - if (hardcodedMatchResult != false) - { - // Either matched a hardcoded string or is explicitly not to be interned. - return hardcodedMatchResult; - } - - if (length > _ginormousThreshold) - { - lock (_ginormous) - { - LinkedListNode current = _ginormous.First; - - while (current != null) - { - if (current.Value.Target is string last && last.Length == candidate.Length && candidate.StartsWithStringByOrdinalComparison(last)) - { - interned = last; - _ginormousHits++; - _ginormousCharsSaved += last.Length; - - _ginormous.Remove(current); - _ginormous.AddFirst(current); - - return true; - } - - current = current.Next; - } - - _ginormousMisses++; - interned = candidate.ExpensiveConvertToString(); - - LinkedListNode lastNode = _ginormous.Last; - _ginormous.RemoveLast(); - _ginormous.AddFirst(lastNode); - lastNode.Value.Target = interned; - - return false; - } - } -#if !CLR2COMPATIBILITY - else if (_useSimpleConcurrency) - { - var stringified = candidate.ExpensiveConvertToString(); - interned = _internedStrings.GetOrAdd(stringified, stringified); - return true; - } -#endif - else if (length >= _hugeMruThreshold) - { - lock (_hugeMru) - { - return _hugeMru.TryGet(candidate, out interned); - } - } - else if (length >= _largeMruThreshold) - { - lock (_largeMru) - { - return _largeMru.TryGet(candidate, out interned); - } - } - else if (length >= _smallMruThreshold) - { - lock (_smallMru) - { - return _smallMru.TryGet(candidate, out interned); - } - } - } - - interned = candidate.ExpensiveConvertToString(); - return null; - } - - /// - /// Version of Intern that gathers statistics - /// - private string InternWithStatistics(T candidate) where T : IInternable - { - lock (_missedStrings) - { - _stopwatch.Start(); - bool? interned = TryIntern(candidate, out string result); - _stopwatch.Stop(); - - if (interned.HasValue && !interned.Value) - { - // Could not intern. - _internMisses++; - - _missedStrings.TryGetValue(result, out int priorCount); - _missedStrings[result] = priorCount + 1; - - return result; - } - else if (interned == null) - { - // Decided not to attempt interning - _internRejects++; - - _rejectedStrings.TryGetValue(result, out int priorCount); - _rejectedStrings[result] = priorCount + 1; - - return result; - } - - _internHits++; - if (!candidate.ReferenceEquals(result)) - { - // Reference changed so 'candidate' is now released and should save memory. - _internEliminatedStrings++; - _internEliminatedChars += candidate.Length; - } - - return result; - } - } - - /// - /// A singly linked list of strings where the most recently accessed string is at the top. - /// Size expands up to a fixed number of strings. - /// - private class PrioritizedStringList - { - /// - /// Maximum size of the mru list. - /// - private readonly int _size; - - /// - /// Head of the mru list. - /// - private Node _mru; - - /// - /// Construct an Mru list with a fixed maximum size. - /// - internal PrioritizedStringList(int size) - { - _size = size; - } - - /// - /// Try to get one element from the list. Upon leaving the function 'candidate' will be at the head of the Mru list. - /// This function is not thread-safe. - /// - internal bool TryGet(T candidate, out string interned) where T : IInternable - { - if (_size == 0) - { - interned = candidate.ExpensiveConvertToString(); - return false; - } - - int length = candidate.Length; - Node secondPrior = null; - Node prior = null; - Node head = _mru; - bool found = false; - int itemCount = 0; - - while (head != null && !found) - { - if (head.Value.Length == length) - { - if (candidate.StartsWithStringByOrdinalComparison(head.Value)) - { - found = true; - } - } - - if (!found) - { - secondPrior = prior; - prior = head; - head = head.Next; - } - - itemCount++; - } - - if (found) - { - // Move it to the top and return the interned version. - if (prior != null) - { - if (!candidate.ReferenceEquals(head.Value)) - { - // Wasn't at the top already, so move it there. - prior.Next = head.Next; - head.Next = _mru; - _mru = head; - interned = _mru.Value; - return true; - } - else - { - // But don't move it up if there is reference equality so that multiple calls to Intern don't redundantly emphasize a string. - interned = head.Value; - return true; - } - } - else - { - // Found the item in the top spot. No need to move anything. - interned = _mru.Value; - return true; - } - } - else - { - // Not found. Create a new entry and place it at the top. - Node old = _mru; - _mru = new Node(candidate.ExpensiveConvertToString()) { Next = old }; - - // Cache miss. Use this opportunity to discard any element over the max size. - if (itemCount >= _size && secondPrior != null) - { - secondPrior.Next = null; - } - - interned = _mru.Value; - return false; - } - } - - /// - /// Returns the number of strings held and the total number of chars held. - /// - internal KeyValuePair Statistics() - { - Node head = _mru; - int chars = 0; - int strings = 0; - while (head != null) - { - chars += head.Value.Length; - strings++; - head = head.Next; - } - - return new KeyValuePair(strings, chars); - } - - /// - /// Singly linked list node. - /// - private class Node - { - /// - /// Construct a Node - /// - internal Node(string value) - { - Value = value; - } - - /// - /// The next node in the list. - /// - internal Node Next { get; set; } - - /// - /// The held string. - /// - internal string Value { get; } - } - } - } - } -} diff --git a/src/Shared/Traits.cs b/src/Shared/Traits.cs index 3522c0972c1..7aa78f0bbeb 100644 --- a/src/Shared/Traits.cs +++ b/src/Shared/Traits.cs @@ -44,16 +44,6 @@ public Traits() /// public readonly bool CacheFileExistence = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MsBuildCacheFileExistence")); - /// - /// Use the legacy string interning implementation based on MRU lists. - /// - public readonly bool UseLegacyStringInterner = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildUseLegacyStringInterner")); - - /// - /// Eliminate locking in OpportunisticIntern at the expense of memory (in effect only if UseLegacyStringInterner is set). - /// - public readonly bool UseSimpleInternConcurrency = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MSBuildUseSimpleInternConcurrency")); - public readonly bool UseSimpleProjectRootElementCacheConcurrency = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MsBuildUseSimpleProjectRootElementCacheConcurrency")); /// diff --git a/src/Shared/WeakStringCache.Concurrent.cs b/src/Shared/WeakStringCache.Concurrent.cs deleted file mode 100644 index 318aeafc131..00000000000 --- a/src/Shared/WeakStringCache.Concurrent.cs +++ /dev/null @@ -1,137 +0,0 @@ - -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using Microsoft.Build.Shared; - -namespace Microsoft.Build -{ - /// - /// Implements the WeakStringCache functionality on modern .NET versions where ConcurrentDictionary is available. - /// - internal sealed partial class WeakStringCache : IDisposable - { - private readonly ConcurrentDictionary _stringsByHashCode; - - public WeakStringCache() - { - _stringsByHashCode = new ConcurrentDictionary(NativeMethodsShared.GetLogicalCoreCount(), _initialCapacity); - } - - /// - /// Main entrypoint of this cache. Tries to look up a string that matches the given internable. If it succeeds, returns - /// the string and sets cacheHit to true. If the string is not found, calls ExpensiveConvertToString on the internable, - /// adds the resulting string to the cache, and returns it, setting cacheHit to false. - /// - /// The internable describing the string we're looking for. - /// true if match found in cache, false otherwise. - /// A string matching the given internable. - /// - /// This method performs two operations on the underlying ConcurrentDictionary on both cache hit and cache miss. - /// 1. It checks whether the dictionary has a matching entry. The entry is temporarily removed from the cache so it doesn't - /// race with Scavenge() freeing GC handles. This is the first operation. - /// 2a. If there is a matching entry, we extract the string out of it and put it back in the cache (the second operation). - /// 2b. If there is an entry but it doesn't match, or there is no entry for the given hash code, we extract the string from - /// the internable, set it on the entry, and add the entry (back) in the cache. - /// - public string GetOrCreateEntry(T internable, out bool cacheHit) where T : IInternable - { - int hashCode = GetInternableHashCode(internable); - - StringWeakHandle handle; - string result; - bool addingNewHandle = false; - - // Get the existing handle from the cache and assume ownership by removing it. We can't use the simple TryGetValue() here because - // the Scavenge method running on another thread could free the handle from underneath us. - if (_stringsByHashCode.TryRemove(hashCode, out handle)) - { - result = handle.GetString(internable); - if (result != null) - { - // We have a hit, put the handle back in the cache. - if (!_stringsByHashCode.TryAdd(hashCode, handle)) - { - // Another thread has managed to add a handle for the same hash code, so the one we got can be freed. - handle.Free(); - } - cacheHit = true; - return result; - } - } - else - { - handle = new StringWeakHandle(); - addingNewHandle = true; - } - - // We don't have the string in the cache - create it. - result = internable.ExpensiveConvertToString(); - - // Set the handle to reference the new string and put it in the cache. - handle.SetString(result); - if (!_stringsByHashCode.TryAdd(hashCode, handle)) - { - // Another thread has managed to add a handle for the same hash code, so the one we got can be freed. - handle.Free(); - } - - // Remove unused handles if our heuristic indicates that it would be productive. Note that the _scavengeThreshold field - // accesses are not protected from races. Being atomic (as guaranteed by the 32-bit data type) is enough here. - if (addingNewHandle) - { - // Prevent the dictionary from growing forever with GC handles that don't reference live strings anymore. - if (_stringsByHashCode.Count >= _scavengeThreshold) - { - // Before we start scavenging set _scavengeThreshold to a high value to effectively lock other threads from - // running Scavenge at the same time (minus rare races). - _scavengeThreshold = int.MaxValue; - try - { - // Get rid of unused handles. - Scavenge(); - } - finally - { - // And do this again when the number of handles reaches double the current after-scavenge number. - _scavengeThreshold = _stringsByHashCode.Count * 2; - } - } - } - - cacheHit = false; - return result; - } - - /// - /// Iterates over the cache and removes unused GC handles, i.e. handles that don't reference live strings. - /// This is expensive so try to call such that the cost is amortized to O(1) per GetOrCreateEntry() invocation. - /// - public void Scavenge() - { - foreach (KeyValuePair entry in _stringsByHashCode) - { - if (!entry.Value.IsUsed && _stringsByHashCode.TryRemove(entry.Key, out StringWeakHandle removedHandle)) - { - // Note that the removed handle may be different from the one we got from the enumerator so check again - // and try to put it back if it's still in use. - if (!removedHandle.IsUsed || !_stringsByHashCode.TryAdd(entry.Key, removedHandle)) - { - removedHandle.Free(); - } - } - } - } - - /// - /// Returns internal debug counters calculated based on the current state of the cache. - /// - public DebugInfo GetDebugInfo() - { - return GetDebugInfoImpl(); - } - } -} diff --git a/src/Shared/WeakStringCache.cs b/src/Shared/WeakStringCache.cs deleted file mode 100644 index 22021e1a08a..00000000000 --- a/src/Shared/WeakStringCache.cs +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. - -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; - -namespace Microsoft.Build -{ - /// - /// A cache of weak GC handles pointing to strings. Weak GC handles are functionally equivalent to WeakReference's but have less overhead - /// (they're a struct as opposed to WR which is a finalizable class) at the expense of requiring manual lifetime management. As long as - /// a string has an ordinary strong GC root elsewhere in the process and another string with the same hashcode hasn't reused the entry, - /// the cache has a reference to it and can match it to an internable. When the string is collected, it is also automatically "removed" - /// from the cache by becoming unrecoverable from the GC handle. GC handles that do not reference a live string anymore are freed lazily. - /// - internal sealed partial class WeakStringCache : IDisposable - { - /// - /// Debug stats returned by GetDebugInfo(). - /// - public struct DebugInfo - { - public int LiveStringCount; - public int CollectedStringCount; - } - - /// - /// Holds a weak GC handle to a string. Shared by all strings with the same hash code and referencing the last such string we've seen. - /// - private struct StringWeakHandle - { - /// - /// Weak GC handle to the last string of the given hashcode we've seen. - /// - public GCHandle WeakHandle; - - /// - /// Returns true if the string referenced by the handle is still alive. - /// - public bool IsUsed => WeakHandle.Target != null; - - /// - /// Returns the string referenced by this handle if it is equal to the given internable. - /// - /// The internable describing the string we're looking for. - /// The string matching the internable or null if the handle is referencing a collected string or the string is different. - public string GetString(T internable) where T : IInternable - { - if (WeakHandle.IsAllocated && WeakHandle.Target is string str) - { - if (internable.Length == str.Length && - internable.StartsWithStringByOrdinalComparison(str)) - { - return str; - } - } - return null; - } - - /// - /// Sets the handle to the given string. If the handle is still referencing another live string, that string is effectively forgotten. - /// - /// The string to set. - public void SetString(string str) - { - if (!WeakHandle.IsAllocated) - { - // The handle is not allocated - allocate it. - WeakHandle = GCHandle.Alloc(str, GCHandleType.Weak); - } - else - { - WeakHandle.Target = str; - } - } - - /// - /// Frees the GC handle. - /// - public void Free() - { - WeakHandle.Free(); - } - } - - /// - /// Initial capacity of the underlying dictionary. - /// - private const int _initialCapacity = 503; - - /// - /// The maximum size we let the collection grow before scavenging unused entries. - /// - private int _scavengeThreshold = _initialCapacity; - - /// - /// Implements the simple yet very decently performing djb2 hash function (xor version). - /// - /// The internable to compute the hash code for. - /// The 32-bit hash code. - internal static int GetInternableHashCode(T internable) where T : IInternable - { - int hashCode = 5381; - for (int i = 0; i < internable.Length; i++) - { - unchecked - { - hashCode = hashCode * 33 ^ internable[i]; - } - } - return hashCode; - } - - /// - /// Frees all GC handles and clears the cache. - /// - public void Dispose() - { - foreach (KeyValuePair entry in _stringsByHashCode) - { - entry.Value.Free(); - } - _stringsByHashCode.Clear(); - } - - /// - /// Returns internal debug counters calculated based on the current state of the cache. - /// - private DebugInfo GetDebugInfoImpl() - { - DebugInfo debugInfo = new DebugInfo(); - - foreach (KeyValuePair entry in _stringsByHashCode) - { - if (entry.Value.IsUsed) - { - debugInfo.LiveStringCount++; - } - else - { - debugInfo.CollectedStringCount++; - } - } - - return debugInfo; - } - } -} diff --git a/src/Tasks/Microsoft.Build.Tasks.csproj b/src/Tasks/Microsoft.Build.Tasks.csproj index b5200e1248b..97daccd3aae 100644 --- a/src/Tasks/Microsoft.Build.Tasks.csproj +++ b/src/Tasks/Microsoft.Build.Tasks.csproj @@ -82,18 +82,6 @@ NGen.cs - - IInternable.cs - - - WeakStringCache.cs - - - WeakStringCache.Concurrent.cs - - - OpportunisticIntern.cs - PropertyParser.cs True diff --git a/src/Utilities/Microsoft.Build.Utilities.csproj b/src/Utilities/Microsoft.Build.Utilities.csproj index 2b851f4526a..2fdd06afdd6 100644 --- a/src/Utilities/Microsoft.Build.Utilities.csproj +++ b/src/Utilities/Microsoft.Build.Utilities.csproj @@ -125,18 +125,6 @@ Shared\InprocTrackingNativeMethods.cs - - Shared\IInternable.cs - - - Shared\WeakStringCache.cs - - - Shared\WeakStringCache.Concurrent.cs - - - Shared\OpportunisticIntern.cs - Shared\ReadOnlyEmptyCollection.cs