/// <summary> /// 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. /// </summary> /// <param name="internable">The internable describing the string we're looking for.</param> /// <returns>A string matching the given internable.</returns> public string GetOrCreateEntry <T>(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); }
/// <summary> /// 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. /// </summary> /// <param name="internable">The internable describing the string we're looking for.</param> /// <param name="cacheHit">true if match found in cache, false otherwise.</param> /// <returns>A string matching the given internable.</returns> public string GetOrCreateEntry(ref InternableString internable, out bool cacheHit) { int hashCode = internable.GetHashCode(); StringWeakHandle?handle; string? result; // Get the existing handle from the cache and lock it while we're dereferencing it to prevent a race with the Scavenge // method running on another thread and freeing the handle from underneath us. if (_stringsByHashCode.TryGetValue(hashCode, out handle)) { lock (handle) { result = handle.GetString(ref internable); if (result != null) { cacheHit = true; return(result); } // We have the handle but it's not referencing the right string - create the right string and store it in the handle. result = internable.ExpensiveConvertToString(); handle.SetString(result); cacheHit = false; return(result); } } // We don't have the handle in the cache - create the right string, store it in the handle, and add the handle to the cache. result = internable.ExpensiveConvertToString(); handle = new StringWeakHandle(); handle.SetString(result); _stringsByHashCode.TryAdd(hashCode, handle); // Remove unused handles if our heuristic indicates that it would be productive. int scavengeThreshold = _scavengeThreshold; 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. if (Interlocked.CompareExchange(ref _scavengeThreshold, int.MaxValue, scavengeThreshold) == scavengeThreshold) { 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); }
/// <summary> /// 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. /// </summary> /// <param name="internable">The internable describing the string we're looking for.</param> /// <param name="cacheHit">true if match found in cache, false otherwise.</param> /// <returns>A string matching the given internable.</returns> /// <remarks> /// 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. /// </remarks> public string GetOrCreateEntry <T>(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); }
/// <summary> /// 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. /// </summary> /// <param name="internable">The internable describing the string we're looking for.</param> /// <returns>A string matching the given internable.</returns> /// <remarks> /// This method performs one operation on the underlying ConcurrentDictionary on cache hit, and two or three operations on cache miss. /// 1. It checks whether the dictionary has a matching entry. This operations is common to all code paths. /// If there is a matching entry we are done. /// 2. If the dictionary doesn't have an entry for the given hash code, we make a new one and add it (the second operation). /// Note that we could do 1. and 2. together using GetOrAdd() with the valueFactory callback but it wouldn't be much faster /// and would require allocating a closure object to share data with the callback. /// 3. If the dictionary has an entry for the given hash code but it doesn't match the argument because it's either already /// collected or there is a hash collision, we have to first remove the existing handle to prevent other threads from /// freeing it (second operation). Only then can it have the target set to the new string and be added back to the dictionary /// (third operation). /// </remarks> public string GetOrCreateEntry <T>(T internable, out bool cacheHit) where T : IInternable { int hashCode = GetInternableHashCode(internable); StringWeakHandle handle; string result; bool addingNewHandle = false; 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(); // If the handle is new, we have to add it to the cache. We do it after removing unused handles if our heuristic // indicates that it would be productive. Note that the _capacity 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) { // Get rid of unused handles. Scavenge(); // And do this again when the number of handles reaches double the current after-scavenge number. _scavengeThreshold = _stringsByHashCode.Count * 2; } } else { // If the handle is already in the cache, we have to be careful because other threads may be operating on it. // In particular the Scavenge method may free the handle from underneath us if we leave it in the cache. if (!_stringsByHashCode.TryRemove(hashCode, out handle)) { // The handle is no longer in the cache so we're creating a new one after all. handle = new StringWeakHandle(); } } // Set the handle to reference the new string and put it in the cache. handle.SetString(result); if (!_stringsByHashCode.TryAdd(hashCode, handle)) { // If somebody beat us to it and the new handle has not been added, free it. handle.Free(); } cacheHit = false; return(result); }