[NativeAOT] Replace GVMLookupForSlot internal cache with generic cache similar to...
authorVladimir Sadov <vsadov@microsoft.com>
Mon, 24 Jul 2023 13:14:23 +0000 (06:14 -0700)
committerGitHub <noreply@github.com>
Mon, 24 Jul 2023 13:14:23 +0000 (06:14 -0700)
* factored out CastCache to be able create several.

* Cache impl

* generic cache

* some refactoring

* separated GenericCache

* comments

* less refs

* do not store hash

* fix CoreCLR and some more refactoring

* PR feedback

* remove no longer needed CastCache wrapping constructor

* remove auto-inserted unused usings.

* remove unused ActivatorCreateInstanceAny

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/CastHelpers.cs
src/coreclr/nativeaot/Runtime.Base/src/System/Runtime/TypeCast.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/Internal/Runtime/CompilerHelpers/LibraryInitializer.cs
src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/TypeLoaderExports.cs
src/coreclr/nativeaot/Test.CoreLib/src/System/Runtime/CompilerServices/CastCache.cs
src/coreclr/vm/corelib.h
src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/CastCache.cs
src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/GenericCache.cs [new file with mode: 0644]

index 435782b..0292c99 100644 (file)
@@ -11,6 +11,9 @@ namespace System.Runtime.CompilerServices
 {
     internal static unsafe class CastHelpers
     {
+        // In coreclr the table is allocated and written to on the native side.
+        internal static int[]? s_table;
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         private static extern object IsInstanceOfAny_NoCacheLookup(void* toTypeHnd, object obj);
 
@@ -36,7 +39,7 @@ namespace System.Runtime.CompilerServices
                 void* mt = RuntimeHelpers.GetMethodTable(obj);
                 if (mt != toTypeHnd)
                 {
-                    CastResult result = CastCache.TryGet((nuint)mt, (nuint)toTypeHnd);
+                    CastResult result = CastCache.TryGet(s_table!, (nuint)mt, (nuint)toTypeHnd);
                     if (result == CastResult.CanCast)
                     {
                         // do nothing
@@ -186,7 +189,7 @@ namespace System.Runtime.CompilerServices
         [MethodImpl(MethodImplOptions.NoInlining)]
         private static object? IsInstance_Helper(void* toTypeHnd, object obj)
         {
-            CastResult result = CastCache.TryGet((nuint)RuntimeHelpers.GetMethodTable(obj), (nuint)toTypeHnd);
+            CastResult result = CastCache.TryGet(s_table!, (nuint)RuntimeHelpers.GetMethodTable(obj), (nuint)toTypeHnd);
             if (result == CastResult.CanCast)
             {
                 return obj;
@@ -215,7 +218,7 @@ namespace System.Runtime.CompilerServices
                 void* mt = RuntimeHelpers.GetMethodTable(obj);
                 if (mt != toTypeHnd)
                 {
-                    result = CastCache.TryGet((nuint)mt, (nuint)toTypeHnd);
+                    result = CastCache.TryGet(s_table!, (nuint)mt, (nuint)toTypeHnd);
                     if (result != CastResult.CanCast)
                     {
                         goto slowPath;
@@ -239,7 +242,7 @@ namespace System.Runtime.CompilerServices
         [MethodImpl(MethodImplOptions.NoInlining)]
         private static object? ChkCast_Helper(void* toTypeHnd, object obj)
         {
-            CastResult result = CastCache.TryGet((nuint)RuntimeHelpers.GetMethodTable(obj), (nuint)toTypeHnd);
+            CastResult result = CastCache.TryGet(s_table!, (nuint)RuntimeHelpers.GetMethodTable(obj), (nuint)toTypeHnd);
             if (result == CastResult.CanCast)
             {
                 return obj;
@@ -456,7 +459,7 @@ namespace System.Runtime.CompilerServices
         [MethodImpl(MethodImplOptions.NoInlining)]
         private static void StelemRef_Helper(ref object? element, void* elementType, object obj)
         {
-            CastResult result = CastCache.TryGet((nuint)RuntimeHelpers.GetMethodTable(obj), (nuint)elementType);
+            CastResult result = CastCache.TryGet(s_table!, (nuint)RuntimeHelpers.GetMethodTable(obj), (nuint)elementType);
             if (result == CastResult.CanCast)
             {
                 WriteBarrier(ref element, obj);
index 3cd06a1..364a248 100644 (file)
@@ -21,8 +21,19 @@ namespace System.Runtime
     //
     /////////////////////////////////////////////////////////////////////////////////////////////////////
 
+    [EagerStaticClassConstruction]
     internal static class TypeCast
     {
+#if DEBUG
+        private const int InitialCacheSize = 8;    // MUST BE A POWER OF TWO
+        private const int MaximumCacheSize = 512;  // make this lower than release to make it easier to reach this in tests.
+#else
+        private const int InitialCacheSize = 128;  // MUST BE A POWER OF TWO
+        private const int MaximumCacheSize = 4096; // 4096 * sizeof(CastCacheEntry) is 98304 bytes on 64bit. We will rarely need this much though.
+#endif // DEBUG
+
+        private static CastCache s_castCache = new CastCache(InitialCacheSize, MaximumCacheSize);
+
         [Flags]
         internal enum AssignmentVariation
         {
@@ -1159,7 +1170,7 @@ namespace System.Runtime
                 return true;
 
             nuint sourceAndVariation = (nuint)pSourceType + (uint)variation;
-            CastResult result = CastCache.TryGet(sourceAndVariation, (nuint)(pTargetType));
+            CastResult result = s_castCache.TryGet(sourceAndVariation, (nuint)(pTargetType));
             if (result != CastResult.MaybeCast)
             {
                 return result == CastResult.CanCast;
@@ -1187,7 +1198,7 @@ namespace System.Runtime
             // Update the cache
             //
             nuint sourceAndVariation = (nuint)pSourceType + (uint)variation;
-            CastCache.TrySet(sourceAndVariation, (nuint)pTargetType, result);
+            s_castCache.TrySet(sourceAndVariation, (nuint)pTargetType, result);
 
             return result;
         }
index 720f0c3..bf3c60d 100644 (file)
@@ -19,7 +19,6 @@ namespace Internal.Runtime.CompilerHelpers
         {
             PreallocatedOutOfMemoryException.Initialize();
             ClassConstructorRunner.Initialize();
-            TypeLoaderExports.Initialize();
         }
     }
 }
index 431c83b..c49220b 100644 (file)
@@ -7,164 +7,173 @@ using System.Diagnostics;
 using System.Threading;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
+using System.Numerics;
 
 namespace System.Runtime
 {
+    // Initialize the cache eagerly to avoid null checks.
+    [EagerStaticClassConstruction]
     public static class TypeLoaderExports
     {
-        public static unsafe void ActivatorCreateInstanceAny(ref object ptrToData, IntPtr pEETypePtr)
+        //
+        // Generic lookup cache
+        //
+
+#if DEBUG
+        // use smaller numbers to hit resizing/preempting logic in debug
+        private const int InitialCacheSize = 8; // MUST BE A POWER OF TWO
+        private const int MaximumCacheSize = 512;
+#else
+        private const int InitialCacheSize = 128; // MUST BE A POWER OF TWO
+        private const int MaximumCacheSize = 128 * 1024;
+#endif // DEBUG
+
+        private static GenericCache<Key, Value> s_cache =
+            new GenericCache<Key, Value>(InitialCacheSize, MaximumCacheSize);
+
+        private struct Key : IEquatable<Key>
         {
-            EETypePtr pEEType = new EETypePtr(pEETypePtr);
+            public IntPtr _context;
+            public IntPtr _signature;
 
-            if (pEEType.IsValueType)
+            public Key(nint context, nint signature)
             {
-                // Nothing else to do for value types.
-                return;
+                _context = context;
+                _signature = signature;
             }
 
-            // For reference types, we need to:
-            //  1- Allocate the new object
-            //  2- Call its default ctor
-            //  3- Update ptrToData to point to that newly allocated object
-            ptrToData = RuntimeImports.RhNewObject(pEEType);
-
-            Entry entry = LookupInCache(s_cache, pEETypePtr, pEETypePtr);
-            entry ??= CacheMiss(pEETypePtr, pEETypePtr,
-                    (IntPtr context, IntPtr signature, object contextObject, ref IntPtr auxResult) =>
-                    {
-                        IntPtr result = RuntimeAugments.TypeLoaderCallbacks.TryGetDefaultConstructorForType(new RuntimeTypeHandle(new EETypePtr(context)));
-                        if (result == IntPtr.Zero)
-                            result = RuntimeAugments.GetFallbackDefaultConstructor();
-                        return result;
-                    });
-            RawCalliHelper.Call(entry.Result, ptrToData);
-        }
+            public bool Equals(Key other)
+            {
+                return _context == other._context && _signature == other._signature;
+            }
 
-        //
-        // Generic lookup cache
-        //
+            public override int GetHashCode()
+            {
+                // pointers will likely match and cancel out in the upper bits
+                // we will rotate context by 16 bit to keep more varying bits in the hash
+                IntPtr context = (IntPtr)BitOperations.RotateLeft((nuint)_context, 16);
+                return (context ^ _signature).GetHashCode();
+            }
 
-        private class Entry
-        {
-            public IntPtr Context;
-            public IntPtr Signature;
-            public IntPtr Result;
-            public IntPtr AuxResult;
-            public Entry Next;
+            public override bool Equals(object obj)
+            {
+                return obj is Key && Equals((Key)obj);
+            }
         }
 
-        // Initialize the cache eagerly to avoid null checks.
-        // Use array with just single element to make this pay-for-play. The actual cache will be allocated only
-        // once the lazy lookups are actually needed.
-        private static Entry[] s_cache;
+        private struct Value
+        {
+            public IntPtr _result;
+            public IntPtr _auxResult;
 
-        private static Lock s_lock;
-        private static GCHandle s_previousCache;
+            public Value(IntPtr result, IntPtr auxResult)
+            {
+                _result = result;
+                _auxResult = auxResult;
+            }
+        }
 
-        internal static void Initialize()
+        private static Value LookupOrAdd(IntPtr context, IntPtr signature)
         {
-            s_cache = new Entry[1];
+            if (!TryGetFromCache(context, signature, out var v))
+            {
+                v = CacheMiss(context, signature);
+            }
+
+            return v;
         }
 
         public static IntPtr GenericLookup(IntPtr context, IntPtr signature)
         {
-            Entry entry = LookupInCache(s_cache, context, signature);
-            entry ??= CacheMiss(context, signature);
-            return entry.Result;
+            if (!TryGetFromCache(context, signature, out var v))
+            {
+                v = CacheMiss(context, signature);
+            }
+
+            return v._result;
         }
 
         public static void GenericLookupAndCallCtor(object arg, IntPtr context, IntPtr signature)
         {
-            Entry entry = LookupInCache(s_cache, context, signature);
-            entry ??= CacheMiss(context, signature);
-            RawCalliHelper.Call(entry.Result, arg);
+            Value v = LookupOrAdd(context, signature);
+            RawCalliHelper.Call(v._result, arg);
         }
 
         public static object GenericLookupAndAllocObject(IntPtr context, IntPtr signature)
         {
-            Entry entry = LookupInCache(s_cache, context, signature);
-            entry ??= CacheMiss(context, signature);
-            return RawCalliHelper.Call<object>(entry.Result, entry.AuxResult);
+            Value v = LookupOrAdd(context, signature);
+            return RawCalliHelper.Call<object>(v._result, v._auxResult);
         }
 
         public static object GenericLookupAndAllocArray(IntPtr context, IntPtr arg, IntPtr signature)
         {
-            Entry entry = LookupInCache(s_cache, context, signature);
-            entry ??= CacheMiss(context, signature);
-            return RawCalliHelper.Call<object>(entry.Result, entry.AuxResult, arg);
+            Value v = LookupOrAdd(context, signature);
+            return RawCalliHelper.Call<object>(v._result, v._auxResult, arg);
         }
 
         public static void GenericLookupAndCheckArrayElemType(IntPtr context, object arg, IntPtr signature)
         {
-            Entry entry = LookupInCache(s_cache, context, signature);
-            entry ??= CacheMiss(context, signature);
-            RawCalliHelper.Call(entry.Result, entry.AuxResult, arg);
+            Value v = LookupOrAdd(context, signature);
+            RawCalliHelper.Call(v._result, v._auxResult, arg);
         }
 
         public static object GenericLookupAndCast(object arg, IntPtr context, IntPtr signature)
         {
-            Entry entry = LookupInCache(s_cache, context, signature);
-            entry ??= CacheMiss(context, signature);
-            return RawCalliHelper.Call<object>(entry.Result, arg, entry.AuxResult);
+            Value v = LookupOrAdd(context, signature);
+            return RawCalliHelper.Call<object>(v._result, arg, v._auxResult);
         }
 
         public static unsafe IntPtr GVMLookupForSlot(object obj, RuntimeMethodHandle slot)
         {
-            Entry entry = LookupInCache(s_cache, (IntPtr)obj.GetMethodTable(), RuntimeMethodHandle.ToIntPtr(slot));
-            if (entry != null)
-                return entry.Result;
+            if (TryGetFromCache((IntPtr)obj.GetMethodTable(), RuntimeMethodHandle.ToIntPtr(slot), out var v))
+                return v._result;
 
             return GVMLookupForSlotSlow(obj, slot);
         }
 
         private static unsafe IntPtr GVMLookupForSlotSlow(object obj, RuntimeMethodHandle slot)
         {
-            Entry entry = CacheMiss((IntPtr)obj.GetMethodTable(), RuntimeMethodHandle.ToIntPtr(slot),
+            Value v = CacheMiss((IntPtr)obj.GetMethodTable(), RuntimeMethodHandle.ToIntPtr(slot),
                     (IntPtr context, IntPtr signature, object contextObject, ref IntPtr auxResult)
                         => RuntimeAugments.TypeLoaderCallbacks.ResolveGenericVirtualMethodTarget(new RuntimeTypeHandle(new EETypePtr(context)), *(RuntimeMethodHandle*)&signature));
 
-            return entry.Result;
+            return v._result;
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static unsafe IntPtr OpenInstanceMethodLookup(IntPtr openResolver, object obj)
         {
-            Entry entry = LookupInCache(s_cache, (IntPtr)obj.GetMethodTable(), openResolver);
-            entry ??= CacheMiss((IntPtr)obj.GetMethodTable(), openResolver,
-                    (IntPtr context, IntPtr signature, object contextObject, ref IntPtr auxResult)
-                        => Internal.Runtime.CompilerServices.OpenMethodResolver.ResolveMethodWorker(signature, contextObject),
-                    obj);
-            return entry.Result;
+            if (!TryGetFromCache((IntPtr)obj.GetMethodTable(), openResolver, out var v))
+            {
+                v = CacheMiss((IntPtr)obj.GetMethodTable(), openResolver,
+                        (IntPtr context, IntPtr signature, object contextObject, ref IntPtr auxResult)
+                            => Internal.Runtime.CompilerServices.OpenMethodResolver.ResolveMethodWorker(signature, contextObject),
+                        obj);
+            }
+
+            return v._result;
         }
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
-        private static Entry LookupInCache(Entry[] cache, IntPtr context, IntPtr signature)
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static bool TryGetFromCache(IntPtr context, IntPtr signature, out Value entry)
         {
-            int key = ((context.GetHashCode() >> 4) ^ signature.GetHashCode()) & (cache.Length - 1);
-#if DEBUG
-            Entry entry = cache[key];
-#else
-            Entry entry = Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(cache), key);
-#endif
-            while (entry != null)
-            {
-                if (entry.Context == context && entry.Signature == signature)
-                    break;
-                entry = entry.Next;
-            }
-            return entry;
+            Key k = new Key(context, signature);
+            return s_cache.TryGet(k, out entry);
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
         internal static IntPtr RuntimeCacheLookupInCache(IntPtr context, IntPtr signature, RuntimeObjectFactory factory, object contextObject, out IntPtr auxResult)
         {
-            Entry entry = LookupInCache(s_cache, context, signature);
-            entry ??= CacheMiss(context, signature, factory, contextObject);
-            auxResult = entry.AuxResult;
-            return entry.Result;
+            if (!TryGetFromCache(context, signature, out var v))
+            {
+                v = CacheMiss(context, signature, factory, contextObject);
+            }
+
+            auxResult = v._auxResult;
+            return v._result;
         }
 
-        private static Entry CacheMiss(IntPtr ctx, IntPtr sig)
+        private static Value CacheMiss(IntPtr ctx, IntPtr sig)
         {
             return CacheMiss(ctx, sig,
                 (IntPtr context, IntPtr signature, object contextObject, ref IntPtr auxResult) =>
@@ -172,161 +181,19 @@ namespace System.Runtime
                 );
         }
 
-        private static unsafe Entry CacheMiss(IntPtr context, IntPtr signature, RuntimeObjectFactory factory, object contextObject = null)
+        private static unsafe Value CacheMiss(IntPtr context, IntPtr signature, RuntimeObjectFactory factory, object contextObject = null)
         {
-            IntPtr result = IntPtr.Zero, auxResult = IntPtr.Zero;
-            bool previouslyCached = false;
-
-            //
-            // Try to find the entry in the previous version of the cache that is kept alive by weak reference
-            //
-            if (s_previousCache.IsAllocated)
-            {
-                Entry[]? previousCache = (Entry[]?)s_previousCache.Target;
-                if (previousCache != null)
-                {
-                    Entry previousEntry = LookupInCache(previousCache, context, signature);
-                    if (previousEntry != null)
-                    {
-                        result = previousEntry.Result;
-                        auxResult = previousEntry.AuxResult;
-                        previouslyCached = true;
-                    }
-                }
-            }
-
             //
             // Call into the type loader to compute the target
             //
-            if (!previouslyCached)
-            {
-                result = factory(context, signature, contextObject, ref auxResult);
-            }
-
-            //
-            // Update the cache under the lock
-            //
-            if (s_lock == null)
-                Interlocked.CompareExchange(ref s_lock, new Lock(), null);
+            IntPtr auxResult = default;
+            IntPtr result = factory(context, signature, contextObject, ref auxResult);
 
-            s_lock.Acquire();
-            try
-            {
-                // Avoid duplicate entries
-                Entry existingEntry = LookupInCache(s_cache, context, signature);
-                if (existingEntry != null)
-                    return existingEntry;
+            Key k = new Key(context, signature);
+            Value v = new Value(result, auxResult);
 
-                // Resize cache as necessary
-                Entry[] cache = ResizeCacheForNewEntryAsNecessary();
-
-                int key = ((context.GetHashCode() >> 4) ^ signature.GetHashCode()) & (cache.Length - 1);
-
-                Entry newEntry = new Entry() { Context = context, Signature = signature, Result = result, AuxResult = auxResult, Next = cache[key] };
-                cache[key] = newEntry;
-                return newEntry;
-            }
-            finally
-            {
-                s_lock.Release();
-            }
-        }
-
-        //
-        // Parameters and state used by generic lookup cache resizing algorithm
-        //
-
-        private const int InitialCacheSize = 128; // MUST BE A POWER OF TWO
-        private const int DefaultCacheSize = 1024;
-        private const int MaximumCacheSize = 128 * 1024;
-
-        private static long s_tickCountOfLastOverflow;
-        private static int s_entries;
-        private static bool s_roundRobinFlushing;
-
-        private static Entry[] ResizeCacheForNewEntryAsNecessary()
-        {
-            Entry[] cache = s_cache;
-
-            if (cache.Length < InitialCacheSize)
-            {
-                // Start with small cache size so that the cache entries used by startup one-time only initialization will get flushed soon
-                return s_cache = new Entry[InitialCacheSize];
-            }
-
-            int entries = s_entries++;
-
-            // If the cache has spare space, we are done
-            if (2 * entries < cache.Length)
-            {
-                if (s_roundRobinFlushing)
-                {
-                    cache[2 * entries] = null;
-                    cache[2 * entries + 1] = null;
-                }
-                return cache;
-            }
-
-            //
-            // Now, we have cache that is overflowing with the stuff. We need to decide whether to resize it or start flushing the old entries instead
-            //
-
-            // Start over counting the entries
-            s_entries = 0;
-
-            // See how long it has been since the last time the cache was overflowing
-            long tickCount = Environment.TickCount64;
-            long tickCountSinceLastOverflow = tickCount - s_tickCountOfLastOverflow;
-            s_tickCountOfLastOverflow = tickCount;
-
-            bool shrinkCache = false;
-            bool growCache = false;
-
-            if (cache.Length < DefaultCacheSize)
-            {
-                // If the cache have not reached the default size, just grow it without thinking about it much
-                growCache = true;
-            }
-            else
-            {
-                if (tickCountSinceLastOverflow < cache.Length / 128)
-                {
-                    // If the fill rate of the cache is faster than ~0.01ms per entry, grow it
-                    if (cache.Length < MaximumCacheSize)
-                        growCache = true;
-                }
-                else
-                if (tickCountSinceLastOverflow > cache.Length * 16)
-                {
-                    // If the fill rate of the cache is slower than 16ms per entry, shrink it
-                    if (cache.Length > DefaultCacheSize)
-                        shrinkCache = true;
-                }
-                // Otherwise, keep the current size and just keep flushing the entries round robin
-            }
-
-            if (growCache || shrinkCache)
-            {
-                s_roundRobinFlushing = false;
-
-                // Keep the reference to the old cache in a weak handle. We will try to use to avoid
-                // hitting the type loader until GC collects it.
-                if (s_previousCache.IsAllocated)
-                {
-                    s_previousCache.Target = cache;
-                }
-                else
-                {
-                    s_previousCache = GCHandle.Alloc(cache, GCHandleType.Weak);
-                }
-
-                return s_cache = new Entry[shrinkCache ? (cache.Length / 2) : (cache.Length * 2)];
-            }
-            else
-            {
-                s_roundRobinFlushing = true;
-                return cache;
-            }
+            s_cache.TrySet(k, v);
+            return v;
         }
     }
 
@@ -334,39 +201,39 @@ namespace System.Runtime
 
     internal static unsafe class RawCalliHelper
     {
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static void Call(System.IntPtr pfn, ref byte data)
             => ((delegate*<ref byte, void>)pfn)(ref data);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static T Call<T>(System.IntPtr pfn, IntPtr arg)
             => ((delegate*<IntPtr, T>)pfn)(arg);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static void Call(System.IntPtr pfn, object arg)
             => ((delegate*<object, void>)pfn)(arg);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static T Call<T>(System.IntPtr pfn, IntPtr arg1, IntPtr arg2)
             => ((delegate*<IntPtr, IntPtr, T>)pfn)(arg1, arg2);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static T Call<T>(System.IntPtr pfn, IntPtr arg1, IntPtr arg2, object arg3, out IntPtr arg4)
             => ((delegate*<IntPtr, IntPtr, object, out IntPtr, T>)pfn)(arg1, arg2, arg3, out arg4);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static void Call(System.IntPtr pfn, IntPtr arg1, object arg2)
             => ((delegate*<IntPtr, object, void>)pfn)(arg1, arg2);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static T Call<T>(System.IntPtr pfn, object arg1, IntPtr arg2)
             => ((delegate*<object, IntPtr, T>)pfn)(arg1, arg2);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static T Call<T>(IntPtr pfn, string[] arg0)
             => ((delegate*<string[], T>)pfn)(arg0);
 
-        [MethodImplAttribute(MethodImplOptions.AggressiveInlining)]
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
         public static ref byte Call(IntPtr pfn, void* arg1, ref byte arg2, ref byte arg3, void* arg4)
             => ref ((delegate*<void*, ref byte, ref byte, void*, ref byte>)pfn)(arg1, ref arg2, ref arg3, arg4);
     }
index 58de4e5..389a8a5 100644 (file)
@@ -8,14 +8,18 @@ namespace System.Runtime.CompilerServices
     }
 
     // trivial implementation of the cast cache
-    internal static unsafe class CastCache
+    internal unsafe struct CastCache
     {
-        internal static CastResult TryGet(nuint source, nuint target)
+        public CastCache(int initialCacheSize, int maxCacheSize)
+        {
+        }
+
+        internal CastResult TryGet(nuint source, nuint target)
         {
             return CastResult.MaybeCast;
         }
 
-        internal static void TrySet(nuint source, nuint target, bool result)
+        internal void TrySet(nuint source, nuint target, bool result)
         {
         }
     }
index d085abd..96576a8 100644 (file)
@@ -1168,7 +1168,7 @@ DEFINE_CLASS(NULLABLE_COMPARER, CollectionsGeneric, NullableComparer`1)
 
 DEFINE_CLASS(INATTRIBUTE, Interop, InAttribute)
 
-DEFINE_CLASS(CASTCACHE, CompilerServices, CastCache)
+DEFINE_CLASS(CASTCACHE, CompilerServices, CastHelpers)
 DEFINE_FIELD(CASTCACHE, TABLE, s_table)
 
 DEFINE_CLASS(CASTHELPERS, CompilerServices, CastHelpers)
index a7b6331..65d65ff 100644 (file)
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\AsyncValueTaskMethodBuilder.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\AsyncValueTaskMethodBuilderT.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\AsyncVoidMethodBuilder.cs" />
-    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\CastCache.cs" Condition="'$(FeatureCoreCLR)' == 'true' or '$(FeatureNativeAot)' == 'true'" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\CastCache.cs" Condition="'$(FeatureMono)' != 'true'" />
+    <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\GenericCache.cs" Condition="'$(FeatureMono)' != 'true'" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\CallerArgumentExpressionAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\CallerFilePathAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Runtime\CompilerServices\CallerLineNumberAttribute.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Numerics\IUnaryPlusOperators.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)System\Numerics\IUnsignedNumber.cs" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
index a333b71..7653749 100644 (file)
@@ -15,47 +15,45 @@ namespace System.Runtime.CompilerServices
         MaybeCast = 2
     }
 
-#if NATIVEAOT
-    [EagerStaticClassConstruction]
-#endif
-    internal static unsafe class CastCache
+    internal unsafe struct CastCache
     {
-
-#if CORECLR
-        // In coreclr the table is written to only on the native side. T
-        // This is all we need to implement TryGet.
-        private static int[]? s_table;
-#else
-
-  #if DEBUG
-        private const int INITIAL_CACHE_SIZE = 8;    // MUST BE A POWER OF TWO
-        private const int MAXIMUM_CACHE_SIZE = 512;  // make this lower than release to make it easier to reach this in tests.
-  #else
-        private const int INITIAL_CACHE_SIZE = 128;  // MUST BE A POWER OF TWO
-        private const int MAXIMUM_CACHE_SIZE = 4096; // 4096 * sizeof(CastCacheEntry) is 98304 bytes on 64bit. We will rarely need this much though.
-  #endif // DEBUG
-
         private const int VERSION_NUM_SIZE = 29;
         private const uint VERSION_NUM_MASK = (1 << VERSION_NUM_SIZE) - 1;
+        private const int BUCKET_SIZE = 8;
 
-        // A trivial 2-elements table used for "flushing" the cache. Nothing is ever stored in this table.
-        // It is required that we are able to allocate this.
-        private static int[] s_sentinelTable = CreateCastCache(2, throwOnFail: true)!;
+        // nothing is ever stored into this, so we can use a static instance.
+        private static int[]? s_sentinelTable;
+
+        // The actual storage.
+        private int[] _table;
 
         // when flushing, remember the last size.
-        private static int s_lastFlushSize = INITIAL_CACHE_SIZE;
+        private int _lastFlushSize;
 
-        // The actual storage.
-        // Initialize to the sentinel in DEBUG as if just flushed, to ensure the sentinel can be handled in Set.
-        private static int[] s_table =
-  #if !DEBUG
-            CreateCastCache(INITIAL_CACHE_SIZE) ??
-  #endif
-            s_sentinelTable;
+        private int _initialCacheSize;
+        private int _maxCacheSize;
 
-#endif // CORECLR
+        public CastCache(int initialCacheSize, int maxCacheSize)
+        {
+            Debug.Assert(BitOperations.PopCount((uint)initialCacheSize) == 1 && initialCacheSize > 1);
+            Debug.Assert(BitOperations.PopCount((uint)maxCacheSize) == 1 && maxCacheSize >= initialCacheSize);
 
-        private const int BUCKET_SIZE = 8;
+            _initialCacheSize = initialCacheSize;
+            _maxCacheSize = maxCacheSize;
+
+            // A trivial 2-elements table used for "flushing" the cache.
+            // Nothing is ever stored in such a small table and identity of the sentinel is not important.
+            // It is required that we are able to allocate this, we may need this in OOM cases.
+            s_sentinelTable ??= CreateCastCache(2, throwOnFail: true);
+
+            _table =
+#if !DEBUG
+            // Initialize to the sentinel in DEBUG as if just flushed, to ensure the sentinel can be handled in Set.
+            CreateCastCache(_initialCacheSize) ??
+#endif
+            s_sentinelTable!;
+            _lastFlushSize = _initialCacheSize;
+        }
 
         [StructLayout(LayoutKind.Sequential)]
         private struct CastCacheEntry
@@ -139,10 +137,17 @@ namespace System.Runtime.CompilerServices
         }
 
         [MethodImpl(MethodImplOptions.AggressiveInlining)]
-        internal static CastResult TryGet(nuint source, nuint target)
+        internal CastResult TryGet(nuint source, nuint target)
+        {
+            // table is always initialized and is not null.
+            return TryGet(_table!, source, target);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal static CastResult TryGet(int[] table, nuint source, nuint target)
         {
             // table is always initialized and is not null.
-            ref int tableData = ref TableData(s_table!);
+            ref int tableData = ref TableData(table);
 
             int index = KeyToBucket(ref tableData, source, target);
             for (int i = 0; i < BUCKET_SIZE;)
@@ -159,7 +164,7 @@ namespace System.Runtime.CompilerServices
 
                 if (entrySource == source)
                 {
-                    // in CoreCLR we do ordinary reads of the entry parts and
+                    // we do ordinary reads of the entry parts and
                     // Interlocked.ReadMemoryBarrier() before reading the version
                     nuint entryTargetAndResult = pEntry._targetAndResult;
                     // target never has its lower bit set.
@@ -204,10 +209,9 @@ namespace System.Runtime.CompilerServices
         // in CoreClr the cache is only updated in the native code
         //
         // The following helpers must match native implementations in castcache.h and castcache.cpp
-#if !CORECLR
 
         // we generally do not OOM in casts, just return null unless throwOnFail is specified.
-        private static int[]? CreateCastCache(int size, bool throwOnFail = false)
+        private int[]? CreateCastCache(int size, bool throwOnFail = false)
         {
             // size must be positive
             Debug.Assert(size > 1);
@@ -225,7 +229,7 @@ namespace System.Runtime.CompilerServices
 
             if (table == null)
             {
-                size = INITIAL_CACHE_SIZE;
+                size = _initialCacheSize;
                 try
                 {
                     table = new int[(size + 1) * sizeof(CastCacheEntry) / sizeof(int)];
@@ -252,20 +256,20 @@ namespace System.Runtime.CompilerServices
             return table;
         }
 
-        internal static void TrySet(nuint source, nuint target, bool result)
+        internal void TrySet(nuint source, nuint target, bool result)
         {
             int bucket;
             ref int tableData = ref *(int*)0;
 
             do
             {
-                tableData = ref TableData(s_table);
+                tableData = ref TableData(_table);
                 if (TableMask(ref tableData) == 1)
                 {
                     // 2-element table is used as a sentinel.
                     // we did not allocate a real table yet or have flushed it.
                     // try replacing the table, but do not insert anything.
-                    MaybeReplaceCacheWithLarger(s_lastFlushSize);
+                    MaybeReplaceCacheWithLarger(_lastFlushSize);
                     return;
                 }
 
@@ -333,7 +337,7 @@ namespace System.Runtime.CompilerServices
             } while (TryGrow(ref tableData));
 
             // reread tableData after TryGrow.
-            tableData = ref TableData(s_table);
+            tableData = ref TableData(_table);
 
             if (TableMask(ref tableData) == 1)
             {
@@ -381,19 +385,22 @@ namespace System.Runtime.CompilerServices
             return TableMask(ref tableData) + 1;
         }
 
-        private static void FlushCurrentCache()
+        private void FlushCurrentCache()
         {
-            ref int tableData = ref TableData(s_table);
+            ref int tableData = ref TableData(_table);
             int lastSize = CacheElementCount(ref tableData);
-            if (lastSize < INITIAL_CACHE_SIZE)
-                lastSize = INITIAL_CACHE_SIZE;
+            if (lastSize < _initialCacheSize)
+                lastSize = _initialCacheSize;
 
-            s_lastFlushSize = lastSize;
+            // store the last size to use when creating a new table
+            // it is just a hint, not needed for correctness, so no synchronization
+            // with the writing of the table
+            _lastFlushSize = lastSize;
             // flushing is just replacing the table with a sentinel.
-            s_table = s_sentinelTable;
+            _table = s_sentinelTable!;
         }
 
-        private static bool MaybeReplaceCacheWithLarger(int size)
+        private bool MaybeReplaceCacheWithLarger(int size)
         {
             int[]? newTable = CreateCastCache(size);
             if (newTable == null)
@@ -401,20 +408,19 @@ namespace System.Runtime.CompilerServices
                 return false;
             }
 
-            s_table = newTable;
+            _table = newTable;
             return true;
         }
 
-        private static bool TryGrow(ref int tableData)
+        private bool TryGrow(ref int tableData)
         {
             int newSize = CacheElementCount(ref tableData) * 2;
-            if (newSize <= MAXIMUM_CACHE_SIZE)
+            if (newSize <= _maxCacheSize)
             {
                 return MaybeReplaceCacheWithLarger(newSize);
             }
 
             return false;
         }
-#endif   // !CORECLR
     }
 }
diff --git a/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/GenericCache.cs b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/GenericCache.cs
new file mode 100644 (file)
index 0000000..712f5ed
--- /dev/null
@@ -0,0 +1,409 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Numerics;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace System.Runtime.CompilerServices
+{
+
+    // EntryInfo is a union, so that we could put some extra info in the element #0 of the table.
+    // the struct is not nested in GenericCache because generic types cannot have explicit layout.
+    [StructLayout(LayoutKind.Explicit)]
+    internal struct EntryInfo
+    {
+        // version has the following structure:
+        // [ distance:3bit |  versionNum:29bit ]
+        //
+        // distance is how many iterations the entry is from its ideal position.
+        // we use that for preemption.
+        //
+        // versionNum is a monotonically increasing numerical tag.
+        // Writer "claims" entry by atomically incrementing the tag. Thus odd number indicates an entry in progress.
+        // Upon completion of adding an entry the tag is incremented again making it even. Even number indicates a complete entry.
+        //
+        // Readers will read the version twice before and after retrieving the entry.
+        // To have a usable entry both reads must yield the same even version.
+        //
+        [FieldOffset(0)]
+        internal uint _version;
+
+        // AuxData  (to store some data specific to the table in the element #0 )
+        [FieldOffset(0)]
+        internal byte hashShift;
+        [FieldOffset(1)]
+        internal byte victimCounter;
+    }
+
+    // NOTE: It is ok if TKey contains references, but we want it to be a struct,
+    //       so that equality is devirtualized.
+    internal unsafe struct GenericCache<TKey, TValue>
+        where TKey: struct, IEquatable<TKey>
+    {
+        private struct Entry
+        {
+            internal EntryInfo _info;
+            internal TKey _key;
+            internal TValue _value;
+
+            [UnscopedRef]
+            public ref uint Version => ref _info._version;
+        }
+
+        private const int VERSION_NUM_SIZE = 29;
+        private const uint VERSION_NUM_MASK = (1 << VERSION_NUM_SIZE) - 1;
+        private const int BUCKET_SIZE = 8;
+
+        // nothing is ever stored into this, so we can use a static instance.
+        private static Entry[]? s_sentinelTable;
+
+        // The actual storage.
+        private Entry[] _table;
+
+        // when flushing, remember the last size.
+        private int _lastFlushSize;
+
+        private int _initialCacheSize;
+        private int _maxCacheSize;
+
+        // creates a new cache instance
+        public GenericCache(int initialCacheSize, int maxCacheSize)
+        {
+            Debug.Assert(BitOperations.PopCount((uint)initialCacheSize) == 1 && initialCacheSize > 1);
+            Debug.Assert(BitOperations.PopCount((uint)maxCacheSize) == 1 && maxCacheSize >= initialCacheSize);
+
+            _initialCacheSize = initialCacheSize;
+            _maxCacheSize = maxCacheSize;
+
+            // A trivial 2-elements table used for "flushing" the cache.
+            // Nothing is ever stored in such a small table and identity of the sentinel is not important.
+            // It is required that we are able to allocate this, we may need this in OOM cases.
+            s_sentinelTable ??= CreateCacheTable(2, throwOnFail: true);
+
+            _table =
+#if !DEBUG
+            // Initialize to the sentinel in DEBUG as if just flushed, to ensure the sentinel can be handled in Set.
+            CreateCacheTable(initialCacheSize) ??
+#endif
+            s_sentinelTable!;
+            _lastFlushSize = initialCacheSize;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static int HashToBucket(Entry[] table, int hash)
+        {
+            byte hashShift = HashShift(table);
+#if TARGET_64BIT
+            return (int)(((ulong)hash * 11400714819323198485ul) >> hashShift);
+#else
+            return (int)(((uint)hash * 2654435769u) >> hashShift);
+#endif
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static ref Entry TableData(Entry[] table)
+        {
+            // points to element 0, which is used for embedded aux data
+            return ref Unsafe.As<byte, Entry>(ref Unsafe.As<RawArrayData>(table).Data);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static ref byte HashShift(Entry[] table)
+        {
+            return ref TableData(table)._info.hashShift;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static ref byte VictimCounter(Entry[] table)
+        {
+            return ref TableData(table)._info.victimCounter;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static int TableMask(Entry[] table)
+        {
+            // element 0 is used for embedded aux data
+            return table.Length - 2;
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static ref Entry Element(Entry[] table, int index)
+        {
+            // element 0 is used for embedded aux data, skip it
+            return ref Unsafe.Add(ref Unsafe.As<byte, Entry>(ref Unsafe.As<RawArrayData>(table).Data), index + 1);
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        internal bool TryGet(TKey key, out TValue? value)
+        {
+            // table is always initialized and is not null.
+            Entry[] table = _table!;
+            int hash = key!.GetHashCode();
+            int index = HashToBucket(table, hash);
+            for (int i = 0; i < BUCKET_SIZE;)
+            {
+                ref Entry pEntry = ref Element(table, index);
+
+                // we must read in this order: version -> [entry parts] -> version
+                // if version is odd or changes, the entry is inconsistent and thus ignored
+                uint version = Volatile.Read(ref pEntry.Version);
+
+                // NOTE: We could store hash as a part of entry info and compare hash before comparing keys.
+                //       Space-wise it would typically be free because of alignment.
+                //       However, hash compare would be advantageous only if it is much cheaper than the key compare.
+                //       That is not the case for current uses of this cache, so for now we do not store
+                //       hash and just do direct comparing of keys. (hash compare can be easily added, if needed)
+                if (key.Equals(pEntry._key))
+                {
+                    // we use ordinary reads to fetch the value
+                    value = pEntry._value;
+
+                    // make sure the second read of 'version' happens after reading '_value'
+                    Interlocked.ReadMemoryBarrier();
+
+                    // mask the lower version bit to make it even.
+                    // This way we can check if version is odd or changing in just one compare.
+                    version &= unchecked((uint)~1);
+                    if (version != pEntry.Version)
+                    {
+                        // oh, so close, the entry is in inconsistent state.
+                        // it is either changing or has changed while we were reading.
+                        // treat it as a miss.
+                        break;
+                    }
+
+                    return true;
+                }
+
+                if (version == 0)
+                {
+                    // the rest of the bucket is unclaimed, no point to search further
+                    break;
+                }
+
+                // quadratic reprobe
+                i++;
+                index = (index + i) & TableMask(table);
+            }
+
+            value = default;
+            return false;
+        }
+
+        // we generally do not want OOM in cache lookups, just return null unless throwOnFail is specified.
+        private Entry[]? CreateCacheTable(int size, bool throwOnFail = false)
+        {
+            // size must be positive
+            Debug.Assert(size > 1);
+            // size must be a power of two
+            Debug.Assert((size & (size - 1)) == 0);
+
+            Entry[]? table = null;
+            try
+            {
+                table = new Entry[size + 1];
+            }
+            catch (OutOfMemoryException) when (!throwOnFail)
+            {
+            }
+
+            if (table == null)
+            {
+                size = _initialCacheSize;
+                try
+                {
+                    table = new Entry[size + 1];
+                }
+                catch (OutOfMemoryException)
+                {
+                }
+            }
+
+            if (table == null)
+            {
+                return table;
+            }
+
+            ref Entry tableData = ref TableData(table);
+
+            // Fibonacci hash reduces the value into desired range by shifting right by the number of leading zeroes in 'size-1'
+            byte shift = (byte)BitOperations.LeadingZeroCount(size - 1);
+            HashShift(table) = shift;
+
+            return table;
+        }
+
+        internal void TrySet(TKey key, TValue value)
+        {
+            int bucket;
+            int hash = key!.GetHashCode();
+            Entry[] table;
+
+            do
+            {
+                table = _table;
+                if (table.Length == 2)
+                {
+                    // 2-element table is used as a sentinel.
+                    // we did not allocate a real table yet or have flushed it.
+                    // try replacing the table, but do not insert anything.
+                    MaybeReplaceCacheWithLarger(_lastFlushSize);
+                    return;
+                }
+
+                bucket = HashToBucket(table, hash);
+                int index = bucket;
+                ref Entry pEntry = ref Element(table, index);
+
+                for (int i = 0; i < BUCKET_SIZE;)
+                {
+                    // claim the entry if unused or is more distant than us from its origin.
+                    // Note - someone familiar with Robin Hood hashing will notice that
+                    //        we do the opposite - we are "robbing the poor".
+                    //        Robin Hood strategy improves average lookup in a lossles dictionary by reducing
+                    //        outliers via giving preference to more distant entries.
+                    //        What we have here is a lossy cache with outliers bounded by the bucket size.
+                    //        We improve average lookup by giving preference to the "richer" entries.
+                    //        If we used Robin Hood strategy we could eventually end up with all
+                    //        entries in the table being maximally "poor".
+
+                    uint version = pEntry.Version;
+
+                    // mask the lower version bit to make it even.
+                    // This way we will detect both if version is changing (odd) or has changed (even, but different).
+                    version &= unchecked((uint)~1);
+
+                    if ((version & VERSION_NUM_MASK) >= (VERSION_NUM_MASK - 2))
+                    {
+                        // If exactly VERSION_NUM_MASK updates happens between here and publishing, we may not recognize a race.
+                        // It is extremely unlikely, but to not worry about the possibility, lets not allow version to go this high and just get a new cache.
+                        // This will not happen often.
+                        FlushCurrentCache();
+                        return;
+                    }
+
+                    if (version == 0 || (version >> VERSION_NUM_SIZE) > i)
+                    {
+                        uint newVersion = ((uint)i << VERSION_NUM_SIZE) + (version & VERSION_NUM_MASK) + 1;
+                        uint versionOrig = Interlocked.CompareExchange(ref pEntry.Version, newVersion, version);
+                        if (versionOrig == version)
+                        {
+                            pEntry._key = key;
+                            pEntry._value = value;
+
+                            // entry is in inconsistent state and cannot be read or written to until we
+                            // update the version, which is the last thing we do here
+                            Volatile.Write(ref pEntry.Version, newVersion + 1);
+                            return;
+                        }
+                        // someone snatched the entry. try the next one in the bucket.
+                    }
+
+                    if (key.Equals(pEntry._key))
+                    {
+                        // looks like we already have an entry for this.
+                        // duplicate entries are harmless, but a bit of a waste.
+                        return;
+                    }
+
+                    // quadratic reprobe
+                    i++;
+                    index += i;
+                    pEntry = ref Element(table, index & TableMask(table));
+                }
+
+                // bucket is full.
+            } while (TryGrow(table));
+
+            // reread tableData after TryGrow.
+            table = _table;
+
+            if (table.Length == 2)
+            {
+                // do not insert into a sentinel.
+                return;
+            }
+
+            // pick a victim somewhat randomly within a bucket
+            // NB: ++ is not interlocked. We are ok if we lose counts here. It is just a number that changes.
+            byte victimDistance = (byte)(VictimCounter(table)++ & (BUCKET_SIZE - 1));
+            // position the victim in a quadratic reprobe bucket
+            int victim = (victimDistance * victimDistance + victimDistance) / 2;
+
+            {
+                ref Entry pEntry = ref Element(table, (bucket + victim) & TableMask(table));
+
+                uint version = pEntry.Version;
+
+                // mask the lower version bit to make it even.
+                // This way we will detect both if version is changing (odd) or has changed (even, but different).
+                version &= unchecked((uint)~1);
+
+                if ((version & VERSION_NUM_MASK) >= (VERSION_NUM_MASK - 2))
+                {
+                    // If exactly VERSION_NUM_MASK updates happens between here and publishing, we may not recognize a race.
+                    // It is extremely unlikely, but to not worry about the possibility, lets not allow version to go this high and just get a new cache.
+                    // This will not happen often.
+                    FlushCurrentCache();
+                    return;
+                }
+
+                uint newVersion = (uint)((victimDistance << VERSION_NUM_SIZE) + (version & VERSION_NUM_MASK) + 1);
+                uint versionOrig = Interlocked.CompareExchange(ref pEntry.Version, newVersion, version);
+
+                if (versionOrig == version)
+                {
+                    pEntry._key = key;
+                    pEntry._value = value;
+                    Volatile.Write(ref pEntry.Version, newVersion + 1);
+                }
+            }
+        }
+
+        private static int CacheElementCount(Entry[] table)
+        {
+            return table.Length - 1;
+        }
+
+        private void FlushCurrentCache()
+        {
+            Entry[] table = _table;
+            int lastSize = CacheElementCount(table);
+            if (lastSize < _initialCacheSize)
+                lastSize = _initialCacheSize;
+
+            // store the last size to use when creating a new table
+            // it is just a hint, not needed for correctness, so no synchronization
+            // with the writing of the table
+            _lastFlushSize = lastSize;
+            // flushing is just replacing the table with a sentinel.
+            _table = s_sentinelTable!;
+        }
+
+        private bool MaybeReplaceCacheWithLarger(int size)
+        {
+            Entry[]? newTable = CreateCacheTable(size);
+            if (newTable == null)
+            {
+                return false;
+            }
+
+            _table = newTable;
+            return true;
+        }
+
+        private bool TryGrow(Entry[] table)
+        {
+            int newSize = CacheElementCount(table) * 2;
+            if (newSize <= _maxCacheSize)
+            {
+                return MaybeReplaceCacheWithLarger(newSize);
+            }
+
+            return false;
+        }
+    }
+}