Repository: dotnet/runtime
File: src/libraries/Microsoft.Extensions.Caching.Memory/src/MemoryCache.cs (C#)

Code before:
        }


        internal sealed class Stats
        {
            private readonly MemoryCache _memoryCache;
            public long Hits;
            public long Misses;

            public Stats(MemoryCache memoryCache)
            {
                _memoryCache = memoryCache;
                _memoryCache.AddToStats(this);
            }

            internal void RemoveFromStats() => _memoryCache.RemoveFromStats(this);
        }

        private void RemoveFromStats(Stats current)
        {
            lock (_allStats!)
            {
                _accumulatedHits += Volatile.Read(ref current.Hits);
                _accumulatedMisses += Volatile.Read(ref current.Misses);

                _allStats.Remove(current);
                _allStats.TrimExcess();
            }
        }

        private void AddToStats(Stats current)
        {
            lock (_allStats!)
            {
                _allStats.Add(current);
            }
        }

        private void ScanForExpiredItems()
        {
            DateTime utcNow = _lastExpirationScan = UtcNow;

            CoherentState coherentState = _coherentState; // Clear() can update the reference in the meantime

            foreach (CacheEntry entry in coherentState.GetAllValues())
            {
                if (entry.CheckExpired(utcNow))
                {
                    coherentState.RemoveEntry(entry, _options);
                }
            }

Review comment (bug):
Maybe it’s a bit of over‑engineering, but we don`t have to always search from the start of the list. We do know the highest possible index of each item, because it’s simply the index where it was originally inserted. Since we only ever add at the end and remove from anywhere, items will always end up at an index equal to or lower than their original index. If we store that index, we can do something like:

```csharp
var searchStart = Math.Min(current.Index, _allStats.Count - 1);
var indexToRemove = _allStats.LastIndexOf(current, searchStart);
Debug.Assert(indexToRemove >= 0, "Stat items must always be at their original index or lower.");
_allStats.RemoveAt(indexToRemove);
```

Reducing how long removals hold the lock means Sum spends less time waiting for it, and the lock becomes less congested when many threads drop items around the same time.

@cincuranet, as the extensions perf-guy in chief, what do you think?

Code after:
        }


        internal sealed class Stats
        {
            private readonly MemoryCache _memoryCache;
            public long Hits;
            public long Misses;
            public int Index;

            public Stats(MemoryCache memoryCache)
            {
                _memoryCache = memoryCache;
                _memoryCache.AddToStats(this);
            }

            internal void RemoveFromStats() => _memoryCache.RemoveFromStats(this);
        }

        private void RemoveFromStats(Stats current)
        {
            lock (_allStats!)
            {
                _accumulatedHits += Volatile.Read(ref current.Hits);
                _accumulatedMisses += Volatile.Read(ref current.Misses);
                int indexToOverwrite = current.Index;
                int lastIndex = _allStats.Count - 1;

                if (indexToOverwrite < lastIndex)
                {
                    _allStats[indexToOverwrite] = _allStats[lastIndex];
                    _allStats[indexToOverwrite].Index = indexToOverwrite;
                }

                _allStats.RemoveAt(lastIndex);
                _allStats.TrimExcess();
            }
        }

        private void AddToStats(Stats current)
        {
            lock (_allStats!)
            {
                current.Index = _allStats.Count;
                _allStats.Add(current);
            }
        }

        private void ScanForExpiredItems()
        {
            DateTime utcNow = _lastExpirationScan = UtcNow;