Skip to content

Commit 6c7c5de

Browse files
Copilotdavidwrightonjkotas
authored
Cache version-resilient hash code on MethodTableAuxiliaryData to fix superlinear behavior with polymorphic recursion (#126534)
`GetVersionResilientTypeHashCode` recomputes hash codes recursively without memoization. With polymorphic recursion, each level doubles the number of unique generic instantiations, causing exponential blowup — `n=31` took ~15s before this fix. On 64-bit release builds, `MethodTableAuxiliaryData` already had 4 bytes of implicit alignment padding between the union and `m_pLoaderModule`. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: davidwrighton <10779849+davidwrighton@users.noreply.github.com> Co-authored-by: David Wrighton <davidwr@microsoft.com> Co-authored-by: Jan Kotas <jkotas@microsoft.com> Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
1 parent 797626d commit 6c7c5de

3 files changed

Lines changed: 39 additions & 19 deletions

File tree

src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,7 @@ internal ref struct ThreadStaticsInfo
11251125
internal unsafe struct MethodTableAuxiliaryData
11261126
{
11271127
private uint Flags;
1128+
private int CachedVersionResilientHashCode;
11281129
private void* LoaderModule;
11291130
private nint ExposedClassObjectRaw;
11301131

src/coreclr/vm/methodtable.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,11 @@ struct MethodTableAuxiliaryData
351351
};
352352
};
353353

354+
// Lazily initialized cache for the version-resilient hash code of this MethodTable.
355+
// A stored value of 0 indicates the field hasn't been set yet.
356+
// Placed here to fill the 4-byte alignment padding between m_dwFlags and m_pLoaderModule,
357+
// so this field adds no extra size to the struct on 64-bit platforms.
358+
int m_cachedVersionResilientHashCode;
354359

355360
PTR_Module m_pLoaderModule;
356361

src/coreclr/vm/versionresilienthashcode.cpp

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,38 +110,52 @@ int GetVersionResilientTypeHashCode(TypeHandle type)
110110
{
111111
STANDARD_VM_CONTRACT;
112112

113-
if (type.IsArray())
114-
{
115-
return ComputeArrayTypeHashCode(GetVersionResilientTypeHashCode(type.GetArrayElementTypeHandle()), type.GetRank());
116-
}
117-
else
118113
if (!type.IsTypeDesc())
119114
{
120115
MethodTable *pMT = type.AsMethodTable();
121116

122-
_ASSERTE(!pMT->IsArray());
123-
_ASSERTE(!IsNilToken(pMT->GetCl()));
124-
125-
LPCUTF8 szNamespace;
126-
LPCUTF8 szName;
127-
IfFailThrow(pMT->GetMDImport()->GetNameOfTypeDef(pMT->GetCl(), &szName, &szNamespace));
128-
int hashcode = ComputeNameHashCode(szNamespace, szName);
117+
_ASSERTE(pMT->IsArray() || !IsNilToken(pMT->GetCl()));
129118

130-
MethodTable *pMTEnclosing = pMT->LoadEnclosingMethodTable(CLASS_LOAD_APPROXPARENTS);
131-
if (pMTEnclosing != NULL)
119+
int cachedHashCode = VolatileLoadWithoutBarrier(&pMT->GetAuxiliaryData()->m_cachedVersionResilientHashCode);
120+
if (cachedHashCode != 0)
132121
{
133-
hashcode = ComputeNestedTypeHashCode(GetVersionResilientTypeHashCode(TypeHandle(pMTEnclosing)), hashcode);
122+
return cachedHashCode;
134123
}
135124

136-
if (!pMT->IsGenericTypeDefinition() && pMT->HasInstantiation())
125+
int hashcode;
126+
if (pMT->IsArray())
137127
{
138-
return ComputeGenericInstanceHashCode(hashcode,
139-
pMT->GetInstantiation().GetNumArgs(), pMT->GetInstantiation(), GetVersionResilientTypeHashCode);
128+
hashcode = ComputeArrayTypeHashCode(GetVersionResilientTypeHashCode(type.GetArrayElementTypeHandle()), type.GetRank());
140129
}
141130
else
142131
{
143-
return hashcode;
132+
LPCUTF8 szNamespace;
133+
LPCUTF8 szName;
134+
IfFailThrow(pMT->GetMDImport()->GetNameOfTypeDef(pMT->GetCl(), &szName, &szNamespace));
135+
hashcode = ComputeNameHashCode(szNamespace, szName);
136+
137+
MethodTable *pMTEnclosing = pMT->LoadEnclosingMethodTable(CLASS_LOAD_APPROXPARENTS);
138+
if (pMTEnclosing != NULL)
139+
{
140+
hashcode = ComputeNestedTypeHashCode(GetVersionResilientTypeHashCode(TypeHandle(pMTEnclosing)), hashcode);
141+
}
142+
143+
if (!pMT->IsGenericTypeDefinition() && pMT->HasInstantiation())
144+
{
145+
hashcode = ComputeGenericInstanceHashCode(hashcode,
146+
pMT->GetInstantiation().GetNumArgs(), pMT->GetInstantiation(), GetVersionResilientTypeHashCode);
147+
}
148+
}
149+
150+
// 0 is used as the sentinel "not yet cached" value, so only cache if non-zero.
151+
// Types with a hash code of exactly 0 are extremely rare in practice; they will simply
152+
// have their hash code recomputed on every call, which is still correct behavior.
153+
if (hashcode != 0)
154+
{
155+
VolatileStore(&pMT->GetAuxiliaryDataForWrite()->m_cachedVersionResilientHashCode, hashcode);
144156
}
157+
158+
return hashcode;
145159
}
146160
else
147161
if (type.IsPointer())

0 commit comments

Comments
 (0)