Cache hash code in TypeStructure to fix O(n) GetHashCode perf regression#19369
Cache hash code in TypeStructure to fix O(n) GetHashCode perf regression#19369T-Gro merged 5 commits intodotnet:mainfrom
Conversation
TypeStructure.GetHashCode was calling GenericHashArbArray on the underlying TypeToken[] on every cache lookup, making each hash O(n). In IDE mode with generative type providers this caused sustained ~150% CPU as the type subsumption cache continuously rehashed all entries. Pre-compute the hash when creating TypeStructure and store it in the DU case. Add [<CustomEquality; NoComparison>] with a fast-path Equals that checks the cached hash before comparing arrays. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
❗ Release notes required
|
Document the FSharp_CacheEvictionImmediate=1 env var workaround for the FCS type subsumption cache hashing bug (dotnet/fsharp#19369). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Lots of calls to |
|
We can get this in, but I am still worried about the "sustained" symptom. Something is creating a lot of unstable tokens which are not found when removing from the store, causing frequent (and likely useless in this scenario?) store rebuilds? |
|
The optimization is ok but I'd make sure the underlying issue repros with current compiler. If I'm not mistaken with current shape of type subsumption cache key: type TTypeCacheKey = TTypeCacheKey of TypeStructure * TypeStructure * CanCoercethere should be no eviction fails whatsoever. That means no rebuilds. |
|
Verified, I went back to F# 9 (still .NET 10) and the issue doesn't repro there. So the rebuildStore symptom was from an older build. The hash caching is still a net win since GetHashCode was O(n) on every cache hit/miss, but yeah the description was overblown. Happy to update it. |
|
This can go in to resolve current pain, but as discussed in the issue - problem of frequent rebuilding is the one to treat, and we would benefit a lot from more test data (repro, or even a project we could attach to our regression testing) |
|
@T-Gro , thanks personally I am stuck to F# 9 for my project. |
|
Are the remaining errors something I can help with? |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
I resolved the errors, this is good to go. |
@dotnet-policy-service agree |
Head branch was pushed to by a user without write access
Summary
TypeStructure.GetHashCodecallsGenericHashArbArrayon the underlyingTypeToken[]on every cache lookup, making each hash computation O(n) over the token array. In IDE mode with generative type providers, the type subsumption cache continuously rehashes all entries duringrebuildStore, causing sustained ~150% CPU.This PR pre-computes the hash when creating
TypeStructureand stores it in the DU case, makingGetHashCodeO(1). It also adds[<CustomEquality; NoComparison>]with a fast-pathEqualsthat checks the cached hash before comparing arrays.Background
A 10-second
dotnet-traceCPU sample on the FSAC process (166% CPU) with a solution using generative type providers shows:Thread.PollGCRuntimeTypeHandle.InternalAllocNoChecksGenericEqualityArbArrayTTypeCacheKey.GetHashCode→GenericHashArbArrayConcurrentDictionary.GrowTableCache.rebuildStorePR #18926 memoized
TypeStructurecreation via aConditionalWeakTable, butGetHashCodestill delegates to F# structural hashing onImmutableArray<TypeToken>(which wrapsT[]), making each call O(n). The cache'srebuildStorere-hashes all existing entries, amplifying the cost.Repro steps
net10.0(LangVersion10) with 5+ projects.dotnet fsautocomplete.dllprocess that does not settle down.Setting
<LangVersion>9.0</LangVersion>(disablingUseTypeSubsumptionCache) eliminates the CPU issue entirely.Changes
src/Compiler/Utilities/TypeHashing.fs: AddedhashTokenArrayhelper. ChangedTypeStructureDU cases to carry a pre-computedhash: int. Added[<CustomEquality; NoComparison>]with O(1)GetHashCodeand fast-pathEquals.src/Compiler/Checking/OverloadResolutionCache.fs: Updated pattern matches for newStable(hash, tokens)/Unstable(hash, tokens)shape.Related