Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Nov 11, 2025

Performance

Measured on 5000 Assert.Equal(x, y) calls where x and y are let-bound integers:

Typecheck Phase Duration

Compiler Typecheck (s) vs .NET 10 vs .NET 9
.NET 9 SDK 40.8 +528% baseline
.NET 10 SDK 6.5 baseline -84%
This PR 2.9 -55% -93%

Speedup: 2.2x faster than .NET 10 SDK, 14x faster than .NET 9 SDK

GC Pressure Reduction

Compiler GC0 collections GC1 GC2
.NET 10 SDK 115 28 3
This PR 21 3 1

GC0 collections reduced by 82%

Cache Statistics

Cache Hit Ratio Hits Misses
overloadResolutionCache 99.98% 4999 1
typeSubsumptionCache 81% 601 141

Why

Overload resolution is expensive. When the same overloaded method is called repeatedly with the same argument types (common in test files with Assert.Equal), the compiler redundantly recomputes the same resolution.

Strategy

Cache the resolution result using a composite key:

  • Method group hash - identity of the available overloads
  • Caller argument types - structural fingerprint of each argument type
  • Object argument types - critical for extension methods
  • Expected return type - for disambiguation (e.g., methods with out args)
  • Type argument count - for generic calls

When the same key is seen again, return the cached winner index.

Handling Type Stability

TypeHashing generates a TypeStructure for each type, which can be:

Structure Meaning Caching Behavior
Stable Fully resolved, won't change ✅ Cache
Unstable (solved typars) Contains type variables that have solutions ✅ Cache (see below)
Unstable (unsolved typars) Contains unsolved flexible typars ❌ Skip
PossiblyInfinite Type exceeds 256 tokens ❌ Skip

Why Solved Typars Are Safe to Cache

When TypeHashing encounters a solved typar (e.g., 'a solved to int), it:

  1. Emits the solution type tokens (int), not the typar itself
  2. Marks the structure as Unstable (because Trace.Undo could theoretically revert it)

However, in overload resolution context, this is safe because:

  • Cache keys are computed before FilterEachThenUndo runs
  • Caller argument types are already resolved at this point
  • Any solved typars in those types won't be reverted by the speculative resolution undo

We reject Unstable structures only when they contain TypeToken.Unsolved - these are flexible typars that could resolve to different types in different contexts.

Before/After Key Strategy

To maximize cache hits:

  1. Compute cache key before resolution (may have solved typars marked Unstable)
  2. Perform resolution (may solve more types)
  3. Compute cache key after resolution (types now fully solved)
  4. Store result under both keys if they differ

This allows future calls with already-solved types to hit the cache directly.

Safety Constraints

Caching is disabled for:

  • Named arguments (complex matching logic)
  • SRTP/trait constraints (resolution is context-dependent)
  • op_Explicit/op_Implicit conversions
  • Single-candidate scenarios (no benefit)

Code Organization

Cache logic is isolated in a dedicated module OverloadResolutionCache.fs:

  • OverloadResolutionCacheKey - cache key type with method hash and type structures
  • tryComputeOverloadCacheKey - key computation with stability checks
  • storeCacheResult - stores under before/after keys for maximum cache hits
  • hasUnsolvedTokens - detects flexible typars that must not be cached

TypeHashing.fs is unchanged from main branch (only MaxTokenCount = 256 constant extracted).

Testing

67 E2E tests verify correct overload selection (not just compilation):

  • 44 basic tests: primitives, multi-args, ParamArray, type hierarchy, extension methods, optional args, rigid typars, tuples, named arguments, constraints, type-directed conversions, out params
  • 23 adversarial tests: cache poisoning attempts with alternating types, generic instantiations, subtype overloads, nested generics, byref overloads, stress test with 100 alternating calls

Each test returns a string identifying which overload was picked, verified at runtime.

Fixes

#18807

Copilot AI and others added 3 commits November 11, 2025 13:31
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Copilot AI changed the title [WIP] Create comprehensive performance profiling automation for xUnit Add automated performance profiling suite for xUnit Assert.Equal compilation issue (#18807) Nov 11, 2025
Copilot AI requested a review from T-Gro November 11, 2025 13:49
…t.Equal test run

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
… path details

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Copilot AI changed the title Add automated performance profiling suite for xUnit Assert.Equal compilation issue (#18807) Add automated performance profiling suite with trace analysis for xUnit Assert.Equal compilation issue (#18807) Nov 11, 2025
…avior

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
… rejected

Co-authored-by: T-Gro <46543583+T-Gro@users.noreply.github.com>
Copilot AI requested a review from T-Gro November 14, 2025 13:36
@github-actions
Copy link
Contributor

github-actions bot commented Jan 20, 2026

❗ Release notes required

@copilot,

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.200.md No release notes found or release notes format is not correct

T-Gro added 4 commits January 27, 2026 16:38
Include the declaring type FullName in the hash computation for
ProvidedMeth in computeMethInfoHash. Previously only the method name
was hashed, which could cause cache collisions between same-named
methods in different type providers.

Changed from: hash (mb.PUntaint((fun m -> m.Name), range0))
To: hash (mb.PUntaint((fun m -> m.Name, (nonNull m.DeclaringType).FullName |> string), range0))
- Add [<Literal>] let MaxTokenCount = 256 constant in HashingPrimitives module
- Replace 10 magic number 256 occurrences with MaxTokenCount
- Improves readability and makes the limit easy to change if needed
- Locations: ResizeArray init, emitNullness, emitStamp, emitMeasure, emitTType
- Add checkSourceHasNoErrors helper at top of OverloadCacheTests.fs
- Replace repetitive boilerplate pattern (getTemporaryFileName + parseAndCheckScript +
  filter errors + shouldBeEmpty) in all 8 test locations
- Helper takes source code, parses and type-checks, asserts no errors, returns checkResults
- Reduces code duplication and improves test maintainability
@dotnet dotnet deleted a comment from Copilot AI Jan 29, 2026
@dotnet dotnet deleted a comment from Copilot AI Jan 29, 2026
@dotnet dotnet deleted a comment from Copilot AI Jan 29, 2026
@dotnet dotnet deleted a comment from Copilot AI Jan 29, 2026
@T-Gro T-Gro changed the title Improve method overload resolution performance by 27% with arity filtering, type pre-checks, lazy evaluation, and caching Cache overload resolution results for repeated method calls Jan 29, 2026
T-Gro added 10 commits January 29, 2026 12:02
The cache key is computed BEFORE FilterEachThenUndo runs in overload
resolution. Any solved typars in caller argument types were established
before overload resolution and won't be reverted by Trace.Undo.

This fixes an overly conservative marking of solved typars as 'unstable',
which was preventing cache hits for the most common case (types that have
already been inferred).

Also adjusts test threshold to 85% to account for additional overload
resolutions that happen for type construction/operators.
This test verifies that the TypeSubsumptionCache (which also uses
TypeHashing) works correctly with solved type parameters after the
change to treat solved typars as stable.

The test covers:
- Type hierarchy with inheritance (Animal/Dog/Cat)
- IEnumerable<T>/IList<T> covariance checks
- Inline generic functions with solved type parameters
- Multiple identical calls to verify cache behavior
Instead of modifying the shared TypeHashing.fs (which is used by both
TypeSubsumptionCache and OverloadResolutionCache), this approach:

1. Restores TypeHashing.fs to original behavior (solved typars mark Unstable)
2. Adds tryGetTypeStructureForOverloadCache in ConstraintSolver.fs that
   accepts both Stable and Unstable structures

This is safe because in the overload resolution context:
- Cache key is computed BEFORE FilterEachThenUndo runs
- Caller argument types were resolved before overload resolution
- Solved typars in those types won't be reverted by Trace.Undo

The TypeSubsumptionCache continues to use tryGetTypeStructureOfStrippedType
directly, which correctly rejects Unstable structures for its use case.
When types are solved during overload resolution, future calls with
already-solved types can now hit the cache directly. The strategy:

1. Compute cache key BEFORE resolution (may have Unstable typars)
2. Lookup using 'before' key
3. Do resolution (may solve typars)
4. Compute cache key AFTER resolution (now Stable)
5. Store under BOTH keys if they differ

This allows:
- First call with unsolved type: misses cache, does resolution, stores
  under both 'before' (unsolved) and 'after' (solved) keys
- Second call with same unsolved pattern: hits 'before' key
- Any call with already-solved types: hits 'after' key directly

Also handles the case where we couldn't compute a 'before' key (types
too unstable) but CAN compute an 'after' key once types are solved.
Tests cover:
- Known vs inferred types at call site
- Generic overloads with explicit type parameters
- Nested generic types (List<int> vs List<string>)
- Multiple identical calls to verify caching

These tests verify the cache produces correct results, not just
that it achieves high hit rates.
The previous code accepted ALL Unstable structures for the overload cache.
This was safe for Unstable-due-to-solved-typars but potentially unsafe for
Unstable-due-to-unsolved-flexible-typars.

Now we check if the tokens contain TypeToken.Unsolved (which indicates a
flexible unsolved typar) and reject those. This ensures:

1. Solved typars (Unstable but tokens contain the solution) → accepted
2. Rigid unsolved typars (Stable, emit TypeToken.Rigid) → accepted
3. Flexible unsolved typars (Unstable, emit TypeToken.Unsolved) → rejected

This prevents potential wrong cache hits when two different unsolved typars
with the same alpha-normalized structure but different constraints resolve
to different overloads.

Also lowered hit rate threshold to 70% to reduce test flakiness when run
with other tests (test isolation issue with metrics listener).
Move cache types and helper functions to OverloadResolutionCache.fs:
- OverloadResolutionCacheKey type
- OverloadResolutionCacheResult type
- getOverloadResolutionCache factory
- tryComputeOverloadCacheKey function
- computeMethInfoHash function
- storeCacheResult function
- hasUnsolvedTokens helper
- tryGetTypeStructureForOverloadCache helper

Keep thin wrapper functions in ConstraintSolver.fs to handle
OverallTy to TType conversion at call sites.

This reduces coupling and prepares for future cache improvements.
- 42 basic tests verifying correct overload selection for primitives, multi-args,
  ParamArray, type hierarchy, extension methods, optional args, rigid typars,
  tuples, named arguments, constraints, and type-directed conversions
- 23 adversarial tests attempting to poison cache with alternating types:
  generic instantiations, subtype overloads, nested generics, byref overloads,
  mixed ParamArray patterns, and stress test with 100 alternating calls
- Tests verify WHICH overload was picked via string indicators, not just compilation
- Source files extracted for proper editor support (syntax highlighting, etc.)
- 44 basic tests verifying correct overload selection for primitives, multi-args,
  ParamArray, type hierarchy, extension methods, optional args, rigid typars,
  tuples, named arguments, constraints, type-directed conversions, and out params
- 23 adversarial tests attempting to poison cache with alternating types:
  generic instantiations, subtype overloads, nested generics, byref overloads,
  mixed ParamArray patterns, and stress test with 100 alternating calls
- Tests verify WHICH overload was picked via string indicators, not just compilation
- Source files extracted for proper editor support (syntax highlighting, etc.)
@T-Gro
Copy link
Member

T-Gro commented Jan 30, 2026

/run fantomas

@T-Gro T-Gro requested a review from abonie January 30, 2026 11:58
@T-Gro T-Gro marked this pull request as ready for review January 30, 2026 11:58
@T-Gro T-Gro requested a review from a team as a code owner January 30, 2026 11:58
@T-Gro T-Gro requested review from majocha and vzarytovskii and removed request for majocha January 30, 2026 11:58
@github-actions
Copy link
Contributor

🔧 CLI Command Report

  • Command: /run fantomas
  • Outcome: success

✅ Patch applied:
- Files changed: 2
- Lines changed: 128

@T-Gro
Copy link
Member

T-Gro commented Jan 30, 2026

@vzarytovskii , @majocha :

I decided to ask you for a review favor here, as this heavily builds upon the work initiated by typesubsumptioncache and in general a long running cache mechanism now available to type check scenarios.

If you have any scenarios in mind, could you please let me know e.g. as a review comment at a test file, and suggest what might be missing?
I will also add some more libs to the regression testing pipeline before this gets merged - IcedTasks and FsToolkit.ErrorHandling for sure. If you have others in mind and think the owners would be fine (or the code's license is fine with it), I can add based on your suggestions even more.

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.

4 participants