diff --git a/src/Compilers/Core/Portable/Collections/ConcurrentCache.cs b/src/Compilers/Core/Portable/Collections/ConcurrentCache.cs index 8b31b486f485f052bed2f4978126e68d7eafb72d..35b0e7dcaaba64922a9699de90caf4a8c679ffa6 100644 --- a/src/Compilers/Core/Portable/Collections/ConcurrentCache.cs +++ b/src/Compilers/Core/Portable/Collections/ConcurrentCache.cs @@ -13,9 +13,10 @@ namespace Microsoft.CodeAnalysis { // very simple cache with a specified size. // expiration policy is "new entry wins over old entry if hashed into the same bucket" - internal class ConcurrentCache : - CachingBase.Entry> where TKey : IEquatable + internal class ConcurrentCache : CachingBase.Entry> { + private readonly IEqualityComparer _keyComparer; + // class, to ensure atomic updates. internal class Entry { @@ -31,18 +32,22 @@ internal Entry(int hash, TKey key, TValue value) } } - public ConcurrentCache(int size) + public ConcurrentCache(int size, IEqualityComparer keyComparer) : base(size) { + _keyComparer = keyComparer; } + public ConcurrentCache(int size) + : this(size, EqualityComparer.Default) { } + public bool TryAdd(TKey key, TValue value) { - var hash = key.GetHashCode(); + var hash = _keyComparer.GetHashCode(key); var idx = hash & mask; var entry = this.entries[idx]; - if (entry != null && entry.hash == hash && entry.key.Equals(key)) + if (entry != null && entry.hash == hash && _keyComparer.Equals(entry.key, key)) { return false; } @@ -53,11 +58,11 @@ public bool TryAdd(TKey key, TValue value) public bool TryGetValue(TKey key, out TValue value) { - int hash = key.GetHashCode(); + int hash = _keyComparer.GetHashCode(key); int idx = hash & mask; var entry = this.entries[idx]; - if (entry != null && entry.hash == hash && entry.key.Equals(key)) + if (entry != null && entry.hash == hash && _keyComparer.Equals(entry.key, key)) { value = entry.value; return true; diff --git a/src/Compilers/Core/Portable/Compilation/Compilation.cs b/src/Compilers/Core/Portable/Compilation/Compilation.cs index f735464508acc4c8c79eb76f7a8d131fa3e65191..5b9a35e4f4b9695d0d82103936d2ed87f867a508 100644 --- a/src/Compilers/Core/Portable/Compilation/Compilation.cs +++ b/src/Compilers/Core/Portable/Compilation/Compilation.cs @@ -902,6 +902,15 @@ public IPointerTypeSymbol CreatePointerTypeSymbol(ITypeSymbol pointedAtType) protected abstract IPointerTypeSymbol CommonCreatePointerTypeSymbol(ITypeSymbol elementType); + // PERF: ETW Traces show that analyzers may use this method frequently, often requesting + // the same symbol over and over again. XUnit analyzers, in particular, were consuming almost + // 1% of CPU time when building Roslyn itself. This is an extremely simple cache that evicts on + // hash code conflicts, but seems to do the trick. The size is mostly arbitrary. My guess + // is that there are maybe a couple dozen analyzers in the solution and each one has + // ~0-2 unique well-known types, and the chance of hash collision is very low. + private ConcurrentCache _getTypeCache = + new ConcurrentCache(50, ReferenceEqualityComparer.Instance); + /// /// Gets the type within the compilation's assembly and all referenced assemblies (other than /// those that can only be referenced via an extern alias) using its canonical CLR metadata name. @@ -912,7 +921,13 @@ public IPointerTypeSymbol CreatePointerTypeSymbol(ITypeSymbol pointedAtType) /// public INamedTypeSymbol GetTypeByMetadataName(string fullyQualifiedMetadataName) { - return CommonGetTypeByMetadataName(fullyQualifiedMetadataName); + if (!_getTypeCache.TryGetValue(fullyQualifiedMetadataName, out var val)) + { + val = CommonGetTypeByMetadataName(fullyQualifiedMetadataName); + // Ignore if someone added the same value before us + _ = _getTypeCache.TryAdd(fullyQualifiedMetadataName, val); + } + return val; } protected abstract INamedTypeSymbol CommonGetTypeByMetadataName(string metadataName);