Skip to content

perf(symbols): share symbols per definition, cache queries - #20845

Open
MangelMaxime wants to merge 6 commits into
dotnet:mainfrom
MangelMaxime:symbols-shared-instances
Open

MangelMaxime wants to merge 6 commits into
dotnet:mainfrom
MangelMaxime:symbols-shared-instances

Conversation

@MangelMaxime

@MangelMaxime MangelMaxime commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

Consumers that walk the typed tree through the symbol API (Fable, analyzers, language servers) get a fresh FSharpMemberOrFunctionOrValue at every call site and a fresh FSharpEntity on every DeclaringEntity, TypeDefinition or AttributeType access, and each symbol recomputes CurriedParameterGroups, Attributes and the like on every call. In a Fable compile of its own test suite those queries were 17 % of all allocated bytes. GetHashCode on a member also hashed its LogicalName, so dictionaries keyed on members collide on every overload.

Within one SymbolEnv, symbols for the same definition are now the same object (generic entities excepted, since each use gets fresh type arguments), their expensive queries are cached, and GetHashCode uses the definition stamp, consistent with the existing Equals. No public API change, and fsc never loads the symbol API.

Workload Before After
SymbolApiBenchmarks.WalkSymbols (added in this PR) 21.3 ms / 60.9 MB 18.9 ms / 55.4 MB
FSharpPlus, tooltip-style queries over all 90k symbol uses 143 ms / 165 MB 108 ms / 142 MB
Fable tests/Js/Main, symbol API allocations 993 MB 737 MB
Fable tests/Js/Main, compile time 4083 ms 3940 ms

Across seven real Fable projects compile time drops 0 to 4 % with byte-identical output.

Checklist

  • Test cases added

  • Performance benchmarks added in case of performance changes

  • Release notes entry updated:

    Please make sure to add an entry with short succinct description of the change as well as link to this pull request to the respective release notes file, if applicable.

    Release notes files:

    • If anything under src/Compiler has been changed, please make sure to make an entry in docs/release-notes/.FSharp.Compiler.Service/<version>.md, where <version> is usually "highest" one, e.g. 42.8.200
    • If language feature was added (i.e. LanguageFeatures.fsi was changed), please add it to docs/release-notes/.Language/preview.md
    • If a change to FSharp.Core was made, please make sure to edit docs/release-notes/.FSharp.Core/<version>.md where version is "highest" one, e.g. 8.0.200.

    Information about the release notes entries format can be found in the documentation.
    Example:

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

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

✅ Release notes checked


✅ Found changes and release notes in following paths:

Change path Release notes path Description
`src/Compiler` docs/release-notes/.FSharp.Compiler.Service/11.0.200.md

Comment thread src/Compiler/Symbols/Symbols.fs Outdated
Comment on lines +67 to +68
let memberSymbols = ConcurrentDictionary<Stamp, obj>()
let entitySymbols = ConcurrentDictionary<Stamp, obj>()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

There are many symbol environments created (and also kept in IDE scenarios). How much can this add to the retained memory?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seems to be very little on the envs an IDE keeps. The member cache is only filled by the expression converter, and ImplementationFile / AssemblyContents build a fresh env per call, so on a file's TypeCheckInfo env it stays empty.

The entity cache fills through DeclaringEntity, TypeDefinition and AttributeType, bounded by the distinct non-generic entities the file touches, and the typed tree already keeps those entities alive.

Measured on FSharpPlus: every file checked with ParseAndCheckFileInProject and all 88 results held, then tooltip-style queries over all 90k symbol uses, twice:

base this PR
heap after checking 88 files 342.0 MB 342.0 MB
growth after querying all symbol uses +0.7 MB +1.7 MB
cache entries - 0 members, 1897 entities (max 177 per file)

About 1 MB for the whole project held open, and the second pass adds nothing. The dictionaries are now created lazily, so an env that never shares a symbol doesn't pay for them.

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ AI review — verify independently.

member _.TypeDefinition =
protect <| fun () ->
match stripTyparEqns ty with
| TType_app (tcref, [], _) -> FSharpEntity.Shared(cenv, tcref)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 🕵️ [P2] Symbol inspection now throws on a missing transitive dependency — Shared's fallback eagerly reads Typars instead of returning the unresolved entity.

// Library exposes ModuleApi.missing : Dim1Dependency.Gone;
// its transitive dim1-Dependency.dll is unavailable to FCS.
let inspect (library: FSharp.Compiler.Symbols.FSharpAssembly) =
    let api =
        library.Contents.Entities
        |> Seq.find (fun e -> e.DisplayName = "ModuleApi")
    let value =
        api.MembersFunctionsAndValues
        |> Seq.find (fun v -> v.DisplayName = "missing")
    value.FullType.TypeDefinition.IsUnresolved
    // Base: true; PR: InvalidOperationException.

@T-Gro T-Gro added the AI-reviewed PR reviewed by AI review council label Oct 23, 2026
@T-Gro
T-Gro self-requested a review October 23, 2026 07:51

@T-Gro T-Gro left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

In general, not just this PR:

Can you pls make PR descriptions more smaller and clearer on what a PR fixes, and less narrating implementation details?

This repo should have a skill for PR descriptions.

(Problem statement + brief problem approach + numerical evidence in case of perf improvements ;; that's all that is needed.)

@github-project-automation github-project-automation Bot moved this from New to In Progress in F# Compiler and Tooling Oct 23, 2026
@MangelMaxime
MangelMaxime force-pushed the symbols-shared-instances branch from 5f3057a to 97f34c4 Compare October 26, 2026 16:25
@MangelMaxime

Copy link
Copy Markdown
Contributor Author

Sorry, I missed the PR skill and wanted to give enough context for where the changes came from.

The description is rewritten with it now, and the bot's finding is fixed with a regression test.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI-reviewed PR reviewed by AI review council

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants