Skip to content

Conversation

majocha
Copy link
Contributor

@majocha majocha commented Aug 29, 2025

implicitYieldExpressions, argInfoCache, typeDefinitelySubsumesNoCoerceCache, typeFeasibleEquivCache

Copy link
Contributor

github-actions bot commented Aug 29, 2025

❗ Release notes required

@majocha,

Caution

No release notes found for the changed paths (see table below).

Please make sure to add an entry with an informative description of the change as well as link to this pull request, issue and language suggestion if applicable. Release notes for this repository are based on Keep A Changelog format.

The following format is recommended for this repository:

* <Informative description>. ([PR #XXXXX](https://github.com/dotnet/fsharp/pull/XXXXX))

See examples in the files, listed in the table below or in th full documentation at https://fsharp.github.io/fsharp-compiler-docs/release-notes/About.html.

If you believe that release notes are not necessary for this PR, please add NO_RELEASE_NOTES label to the pull request.

You can open this PR in browser to add release notes: open in github.dev

Change path Release notes path Description
src/Compiler docs/release-notes/.FSharp.Compiler.Service/10.0.100.md No release notes found or release notes format is not correct
vsintegration/src docs/release-notes/.VisualStudio/18.0.md No release notes found or release notes format is not correct

// Is it quadratic or quasi-quadratic?
if ValueIsUsedOrHasEffect cenv (fun () -> (freeInExpr (CollectLocalsWithStackGuard()) bodyR).FreeLocals) (bindR, bindingInfo) then
let collect expr = (freeInExpr (CollectLocalsWithStackGuard()) expr).FreeLocals
if ValueIsUsedOrHasEffect cenv (fun () -> (getFreeLocalsCache cenv).GetOrAdd(bodyR, collect)) (bindR, bindingInfo) then
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gives a modest 20% hit ratio in tests. May or may not help somewhat IRL but needs benchmarking.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at Optimization times when building FCS I don't see any speedup.

/// Usage:
/// let getValueFor = WeakMap.getOrCreate (fun () -> expensiveInit())
/// let v = getValueFor someKey
let getOrCreate valueFactory =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allows to quickly and and-hoc attach a cache instance in various places. Currently we thread various caches along with context objects / records which is not that convenient when testing things out.

Comment on lines +958 to +961
let getArgInfoCache =
let options = Caches.CacheOptions.getDefault() |> Caches.CacheOptions.withNoEviction
let factory _ = new Caches.Cache<_, ArgReprInfo>(options, "argInfoCache")
WeakMap.getOrCreate factory
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these caches going to be cleared when asking FCS to clear its caches?

Copy link
Contributor Author

@majocha majocha Aug 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently not, I think since they're weakly attached to higher lever stuff, they'll just get GC'd with it. I captured some telemetry and the caches get disposed quite often during the test run.

@majocha
Copy link
Contributor Author

majocha commented Aug 30, 2025

Ahh, there's that memory leak in vs tests, again 😔

env.eCachedImplicitYieldExpressions.Add(synExpr1.Range, (synExpr1, expr1Ty, cachedExpr))
try TcExpr cenv overallTy env tpenv otherExpr
finally env.eCachedImplicitYieldExpressions.Remove synExpr1.Range
(getImplicitYieldExpressionsCache cenv).AddOrUpdate(synExpr1, (expr1Ty, cachedExpr))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantics w.r.t. to error handling is different now, aren't they?

Also it feels like the previous version had a race condition (FindAll and later Add), possibly the Multi map was chosen to workaround it by storing a list and not a single value?

Copy link
Contributor Author

@majocha majocha Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previous version had some concurrency problems, because Vlad at some point added ConcurrentDictionary as HashMultiMap backing store to fix them.

Semantics are different. Originally this just always removed the value in finally. I have to revisit this, I remember the change made sense to me but I forgot why, oh lol. I guess my thinking was eviction will be sufficient here. Now I also notice this was attached to env, not cenv.

This is not really tested in the test suite but there is a benchmark. I'll run it later to see if this still works.

@majocha
Copy link
Contributor Author

majocha commented Sep 11, 2025

I'm trying to identify places that could benefit from memoization or better caching.

Here are some suggestions by Copilot:

InfoReader: immediate members for a type

  • What: Cache “immediate” metadata enumerations per type:
    • GetImmediateIntrinsicMethInfosOfTypeAux, GetImmediateIntrinsicPropInfosOfTypeAux
    • GetImmediateIntrinsicILFieldsOfType, ComputeImmediateIntrinsicEventsOfType
  • Why: We repeatedly re-enumerate IL metadata and F# members by name for the same types inside hierarchy folds and during overload resolution.
  • Where: src/Compiler/Checking/InfoReader.fs
  • Key:
    • (TypeStructure, optional name filter) for the raw unfiltered enumeration.
    • Do NOT key by AccessorDomain; cache the raw list and filter per call (Is*Accessible).
  • Cache result:
    • MethInfo list (per name or “all”); PropInfo list; ILFieldInfo list; EventInfo list.
  • Notes:
    • Factor the existing “…Uncached” helpers to call cached “get all by name” then filter.
    • Works well for IL types; for F# types, MemberInfo is stable after typecheck.

Method/Property sets along hierarchy

  • What: Cache folded results:
    • GetIntrinsicMethodSetsUncached / GetIntrinsicPropertySetsUncached (and named variants)
  • Why: Same fold gets repeated across overload/resolution passes and “most specific” filtering.
  • Where: src/Compiler/Checking/InfoReader.fs
  • Key:
    • (TypeStructure, optional name, AllowMultiIntfInstantiations) → list<list>
    • Keep raw sets; filter by AccessorDomain and “most specific” per call.
  • Notes:
    • Avoid duplicating per‑call sorting by signature; cache canonicalized lists and reuse.

“Most specific” and equivalence checks for methods

  • What: Memoize expensive equivalence/specificity predicates:
    • MethInfosEquivByPartialSig, PropInfosEquivByNameAndPartialSig, GetMostSpecificItemsByType
  • Why: These perform lots of structural type/equivalence checks in nested loops.
  • Where: src/Compiler/Checking/InfoReader.fs
  • Key:
    • Pairwise (minfo1, minfo2) plus Erasure mode → bool
  • Notes:
    • Use a scoped LRU per compilation to avoid unbounded growth.

NameResolution: type lookup tables

  • What: Cache per‑type “named items” aggregation:
    • GetIntrinsicNamedItemsUncached
  • Why: It collects methods, properties, fields, events, traits by name and folds the hierarchy.
  • Where: src/Compiler/Checking/InfoReader.fs / NameResolution.fs
  • Key:
    • (TypeStructure, name) → HierarchyItem
  • Notes:
    • Again, filter accessibility at call sites.

I also notice the existing caching in InfoReader seem to exclude generic types. This can't be good. I wonder if it is possible to improve with similar approach to keys / hashing like in type subsumption cache.

@majocha
Copy link
Contributor Author

majocha commented Sep 11, 2025

With aditional type relations caches there seem to be a slight speedup against main:

DecentlySizedStandAloneFileBenchmark

cache-2

| Method | Mean     | Error    | StdDev   | Gen0      | Gen1      | Gen2      | Allocated |
|------- |---------:|---------:|---------:|----------:|----------:|----------:|----------:|
| Run    | 184.8 ms | 10.26 ms | 29.76 ms | 2000.0000 | 1000.0000 | 1000.0000 | 227.98 MB |

| Method | Mean     | Error    | StdDev   | Gen0      | Gen1      | Gen2      | Allocated |
|------- |---------:|---------:|---------:|----------:|----------:|----------:|----------:|
| Run    | 192.5 ms | 10.54 ms | 31.07 ms | 2000.0000 | 1000.0000 | 1000.0000 | 227.95 MB |

| Method | Mean     | Error    | StdDev   | Gen0      | Gen1      | Gen2      | Allocated |
|------- |---------:|---------:|---------:|----------:|----------:|----------:|----------:|
| Run    | 189.5 ms | 10.63 ms | 30.84 ms | 2000.0000 | 1000.0000 | 1000.0000 |    228 MB |

main

| Method | Mean     | Error   | StdDev   | Gen0      | Gen1      | Gen2      | Allocated |
|------- |---------:|--------:|---------:|----------:|----------:|----------:|----------:|
| Run    | 203.5 ms | 8.23 ms | 24.01 ms | 3000.0000 | 2000.0000 | 1000.0000 | 227.19 MB |

| Method | Mean     | Error   | StdDev   | Gen0      | Gen1      | Gen2      | Allocated |
|------- |---------:|--------:|---------:|----------:|----------:|----------:|----------:|
| Run    | 204.7 ms | 8.61 ms | 24.99 ms | 3000.0000 | 2000.0000 | 1000.0000 |    227 MB |

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: New
Development

Successfully merging this pull request may close these issues.

3 participants