|
16 | 16 | #include <limits> |
17 | 17 | #include <functional> |
18 | 18 |
|
| 19 | +struct InterpDispatchCacheEntry |
| 20 | +{ |
| 21 | + // MethodTable of the calling object |
| 22 | + MethodTable* pMT; |
| 23 | + size_t dispatchToken; |
| 24 | + // Resolved target MethodDesc |
| 25 | + MethodDesc* pTargetMD; |
| 26 | + // Used for linking dead entries for cleanup during GC |
| 27 | + InterpDispatchCacheEntry* pNextDead; |
| 28 | + |
| 29 | + InterpDispatchCacheEntry(MethodTable* pMT, size_t dispatchToken, MethodDesc* pTargetMD) |
| 30 | + { |
| 31 | + this->pMT = pMT; |
| 32 | + this->dispatchToken = dispatchToken; |
| 33 | + this->pTargetMD = pTargetMD; |
| 34 | + pNextDead = nullptr; |
| 35 | + } |
| 36 | +}; |
| 37 | + |
| 38 | +#define DISPATCH_CACHE_BITS 12 |
| 39 | +#define DISPATCH_CACHE_SIZE (1 << DISPATCH_CACHE_BITS) |
| 40 | +#define DISPATCH_CACHE_MASK (DISPATCH_CACHE_SIZE - 1) |
| 41 | + |
| 42 | +// A simple hash table that caches virtual method dispatch results. |
| 43 | +// Maps (DispatchToken, MethodTable*) to MethodDesc*. |
| 44 | +struct InterpDispatchCache |
| 45 | +{ |
| 46 | + InterpDispatchCacheEntry* m_cache[DISPATCH_CACHE_SIZE]; |
| 47 | + // List of dead entries to be freed at GC time |
| 48 | + InterpDispatchCacheEntry* m_pDeadList; |
| 49 | + |
| 50 | + MethodDesc* Lookup(size_t dispatchToken, void* pMT, uint16_t dispatchTokenHash) |
| 51 | + { |
| 52 | + LIMITED_METHOD_CONTRACT; |
| 53 | + |
| 54 | + size_t idx = Hash(dispatchToken, pMT, dispatchTokenHash); |
| 55 | + |
| 56 | + InterpDispatchCacheEntry* pEntry = VolatileLoadWithoutBarrier(&m_cache[idx]); |
| 57 | + // Data dependency ensures field reads are ordered after loading of `pEntry` |
| 58 | + // The entry struct is immutable once created, so these reads are safe |
| 59 | + if (pEntry != nullptr && pEntry->pMT == pMT && pEntry->dispatchToken == dispatchToken) |
| 60 | + return pEntry->pTargetMD; |
| 61 | + |
| 62 | + return NULL; |
| 63 | + } |
| 64 | + |
| 65 | + void Insert(size_t dispatchToken, MethodTable* pMT, MethodDesc* pTargetMD, uint16_t dispatchTokenHash) |
| 66 | + { |
| 67 | + LIMITED_METHOD_CONTRACT; |
| 68 | + |
| 69 | + size_t idx = Hash(dispatchToken, pMT, dispatchTokenHash); |
| 70 | + |
| 71 | + InterpDispatchCacheEntry* pNewEntry = new (nothrow) InterpDispatchCacheEntry(pMT, dispatchToken, pTargetMD); |
| 72 | + if (pNewEntry == nullptr) |
| 73 | + return; |
| 74 | + |
| 75 | + // CAS has release semantics, so the fields of the entry have correct |
| 76 | + // values once the entry is published. If CAS succeeds, we own the old |
| 77 | + // entry for freeing |
| 78 | + InterpDispatchCacheEntry* pOldEntry = InterlockedExchangeT(&m_cache[idx], pNewEntry); |
| 79 | + |
| 80 | + if (pOldEntry != nullptr) |
| 81 | + AddToDeadList(pOldEntry); |
| 82 | + } |
| 83 | + |
| 84 | + // Called during GC sync point to free dead entries |
| 85 | + // At this point, no other threads are running, so it is safe to free |
| 86 | + void ReclaimDeadEntries() |
| 87 | + { |
| 88 | + LIMITED_METHOD_CONTRACT; |
| 89 | + |
| 90 | + InterpDispatchCacheEntry* pDeadList = VolatileLoadWithoutBarrier(&m_pDeadList); |
| 91 | + |
| 92 | + while (pDeadList != nullptr) |
| 93 | + { |
| 94 | + InterpDispatchCacheEntry* pNext = pDeadList->pNextDead; |
| 95 | + delete pDeadList; |
| 96 | + pDeadList = pNext; |
| 97 | + } |
| 98 | + |
| 99 | + VolatileStoreWithoutBarrier(&m_pDeadList, (InterpDispatchCacheEntry*)nullptr); |
| 100 | + } |
| 101 | + |
| 102 | + // Add an entry to the dead list for later cleanup |
| 103 | + void AddToDeadList(InterpDispatchCacheEntry* pEntry) |
| 104 | + { |
| 105 | + LIMITED_METHOD_CONTRACT; |
| 106 | + |
| 107 | + InterpDispatchCacheEntry* pOldHead; |
| 108 | + do |
| 109 | + { |
| 110 | + pOldHead = VolatileLoadWithoutBarrier(&m_pDeadList); |
| 111 | + pEntry->pNextDead = pOldHead; |
| 112 | + } |
| 113 | + while (InterlockedCompareExchangeT(&m_pDeadList, pEntry, pOldHead) != pOldHead); |
| 114 | + } |
| 115 | + |
| 116 | + // Same as VSD DispatchCache's HashToken + HashMT |
| 117 | + static uint16_t Hash(size_t dispatchToken, void* pMT, uint16_t dispatchTokenHash) |
| 118 | + { |
| 119 | + LIMITED_METHOD_CONTRACT; |
| 120 | + |
| 121 | + size_t mtHash = (size_t)pMT; |
| 122 | + mtHash = (((mtHash >> DISPATCH_CACHE_BITS) + mtHash) >> LOG2_PTRSIZE) & DISPATCH_CACHE_MASK; |
| 123 | + |
| 124 | + uint16_t hash = (uint16_t)mtHash; |
| 125 | + hash ^= (dispatchTokenHash & DISPATCH_CACHE_MASK); |
| 126 | + |
| 127 | + return hash; |
| 128 | + } |
| 129 | + |
| 130 | + void ClearEntriesForLoaderAllocator(LoaderAllocator* pLoaderAllocator) |
| 131 | + { |
| 132 | + LIMITED_METHOD_CONTRACT; |
| 133 | + |
| 134 | + for (size_t i = 0; i < DISPATCH_CACHE_SIZE; i++) |
| 135 | + { |
| 136 | + InterpDispatchCacheEntry* pEntry = VolatileLoadWithoutBarrier(&m_cache[i]); |
| 137 | + if (pEntry == nullptr) |
| 138 | + continue; |
| 139 | + |
| 140 | + if (pEntry->pMT->GetLoaderAllocator() == pLoaderAllocator || |
| 141 | + pEntry->pTargetMD->GetLoaderAllocator() == pLoaderAllocator) |
| 142 | + { |
| 143 | + VolatileStoreWithoutBarrier(&m_cache[i], (InterpDispatchCacheEntry*)nullptr); |
| 144 | + // Given the EE is suspended, we can free the entry without worrying about races |
| 145 | + delete pEntry; |
| 146 | + } |
| 147 | + } |
| 148 | + } |
| 149 | +}; |
| 150 | + |
| 151 | +// Global interpreter dispatch cache instance |
| 152 | +static InterpDispatchCache g_InterpDispatchCache; |
| 153 | + |
| 154 | +// Called during GC, when we are guaranteed no entry is being used by any thread. |
| 155 | +void InterpDispatchCache_ReclaimAll() |
| 156 | +{ |
| 157 | + CONTRACTL |
| 158 | + { |
| 159 | + NOTHROW; |
| 160 | + GC_NOTRIGGER; |
| 161 | + MODE_COOPERATIVE; |
| 162 | + // Should only be called during a GC suspension |
| 163 | + PRECONDITION(Debug_IsLockedViaThreadSuspension()); |
| 164 | + } |
| 165 | + CONTRACTL_END; |
| 166 | + |
| 167 | + g_InterpDispatchCache.ReclaimDeadEntries(); |
| 168 | +} |
| 169 | + |
| 170 | +// Clear entries that reference types/methods from the given LoaderAllocator. |
| 171 | +// Called during collectible assembly unload when the EE is suspended. |
| 172 | +void InterpDispatchCache_ClearForLoaderAllocator(LoaderAllocator* pLoaderAllocator) |
| 173 | +{ |
| 174 | + g_InterpDispatchCache.ClearEntriesForLoaderAllocator(pLoaderAllocator); |
| 175 | +} |
| 176 | + |
| 177 | +static size_t CreateDispatchTokenForMethod(MethodDesc* pMD) |
| 178 | +{ |
| 179 | + WRAPPER_NO_CONTRACT; |
| 180 | + |
| 181 | + uint32_t slotNumber = pMD->GetSlot(); |
| 182 | + |
| 183 | + if (pMD->IsInterface()) |
| 184 | + { |
| 185 | + // For interface methods, get the interface's TypeID |
| 186 | + MethodTable* pInterfaceMT = pMD->GetMethodTable(); |
| 187 | + uint32_t typeID = pInterfaceMT->GetTypeID(); |
| 188 | + return DispatchToken::CreateDispatchToken(typeID, slotNumber).To_SIZE_T(); |
| 189 | + } |
| 190 | + else |
| 191 | + { |
| 192 | + // For non-interface virtual methods, use TYPE_ID_THIS_CLASS |
| 193 | + return DispatchToken::CreateDispatchToken(slotNumber).To_SIZE_T(); |
| 194 | + } |
| 195 | +} |
| 196 | + |
19 | 197 | #ifdef TARGET_WASM |
20 | 198 | // Unused on WASM |
21 | 199 | #define SAVE_THE_LOWEST_SP do {} while (0) |
@@ -2686,15 +2864,38 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr |
2686 | 2864 | OBJECTREF *pThisArg = LOCAL_VAR_ADDR(callArgsOffset, OBJECTREF); |
2687 | 2865 | NULL_CHECK(*pThisArg); |
2688 | 2866 |
|
2689 | | - // Interpreter-TODO |
2690 | | - // This needs to be optimized, not operating at MethodDesc level, rather with ftnptr |
2691 | | - // slots containing the interpreter IR pointer |
2692 | | - targetMethod = CallWithSEHWrapper( |
2693 | | - [&pMD, &pThisArg]() { |
2694 | | - return pMD->GetMethodDescOfVirtualizedCode(pThisArg, pMD->GetMethodTable()); |
2695 | | - }); |
| 2867 | + MethodTable *pObjMT = (*pThisArg)->GetMethodTable(); |
2696 | 2868 |
|
2697 | | - ip += 4; |
| 2869 | + // Interpreter-FIXME: It would be nice to have these caches initialized at compile time instead |
| 2870 | + // Obtain the cached dispatch token or initialize it |
| 2871 | + size_t dispatchToken = (size_t)VolatileLoadWithoutBarrier(&pMethod->pDataItems[ip[4]]); |
| 2872 | + if (dispatchToken == 0) |
| 2873 | + { |
| 2874 | + dispatchToken = CreateDispatchTokenForMethod(pMD); |
| 2875 | + VolatileStoreWithoutBarrier(&pMethod->pDataItems[ip[4]], (void*)dispatchToken); |
| 2876 | + } |
| 2877 | + // The token hash is cached in the data item immediately following the dispatchToken |
| 2878 | + size_t dispatchTokenHash = (size_t)VolatileLoadWithoutBarrier(&pMethod->pDataItems[ip[4] + 1]); |
| 2879 | + if (dispatchTokenHash == 0) |
| 2880 | + { |
| 2881 | + dispatchTokenHash = DispatchToken::From_SIZE_T(dispatchToken).GetHash(); |
| 2882 | + VolatileStoreWithoutBarrier(&pMethod->pDataItems[ip[4] + 1], (void*)dispatchTokenHash); |
| 2883 | + } |
| 2884 | + |
| 2885 | + // Try cache lookup first |
| 2886 | + targetMethod = g_InterpDispatchCache.Lookup(dispatchToken, pObjMT, (uint16_t)dispatchTokenHash); |
| 2887 | + |
| 2888 | + if (targetMethod == NULL) |
| 2889 | + { |
| 2890 | + // miss, resolve the virtual method and cache it |
| 2891 | + targetMethod = CallWithSEHWrapper( |
| 2892 | + [&pMD, &pThisArg]() { |
| 2893 | + return pMD->GetMethodDescOfVirtualizedCode(pThisArg, pMD->GetMethodTable()); |
| 2894 | + }); |
| 2895 | + g_InterpDispatchCache.Insert(dispatchToken, pObjMT, targetMethod, (uint16_t)dispatchTokenHash); |
| 2896 | + } |
| 2897 | + |
| 2898 | + ip += 5; |
2698 | 2899 | goto CALL_INTERP_METHOD; |
2699 | 2900 | } |
2700 | 2901 |
|
@@ -2852,7 +3053,6 @@ void InterpExecMethod(InterpreterFrame *pInterpreterFrame, InterpMethodContextFr |
2852 | 3053 | [&targetMethod, &pThisArg]() { |
2853 | 3054 | return targetMethod->GetMethodDescOfVirtualizedCode(pThisArg, targetMethod->GetMethodTable()); |
2854 | 3055 | }); |
2855 | | - |
2856 | 3056 | } |
2857 | 3057 | else |
2858 | 3058 | { |
|
0 commit comments