Skip to content

[Repo Assist] perf: avoid O(n²) allocations in ILMethodDefs; use lazy caches in TargetTypeDefinition member lookups - #497

Merged
dsyme merged 2 commits into
masterfrom
repo-assist/perf-improve-targettype-lookups-20260403-c3ef47a-c6d0b490e2ae80ae
Apr 3, 2026
Merged

[Repo Assist] perf: avoid O(n²) allocations in ILMethodDefs; use lazy caches in TargetTypeDefinition member lookups#497
dsyme merged 2 commits into
masterfrom
repo-assist/perf-improve-targettype-lookups-20260403-c3ef47a-c6d0b490e2ae80ae

Conversation

@github-actions

@github-actions github-actions Bot commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

🤖 This is an automated PR from Repo Assist.

Summary

Two complementary performance improvements to TargetTypeDefinition and the internal ILMethodDefs name-index.

1. O(1)-per-entry method-name index construction (ILMethodDefs.lmap)

Before: the lazy name→methods dictionary was built by prepending each new entry to an existing array:

m.[key] <- Array.append [| y |] lmpak   // O(n) per insertion

For a type with N methods sharing a name, this allocates arrays of sizes 1, 2, 3 … N — O(N²) total elements. The BCL contains types (e.g. String, Convert) with dozens of overloads per name, making this measurable.

After: a ResizeArray accumulator collects entries per name in O(1) each, then a single ToArray() call per bucket produces the final arrays — O(N) total elements.

2. Lazy-cache reuse in GetField, GetPropertyImpl, GetEvent

Before: each call to GetField/GetPropertyImpl/GetEvent scanned the raw ILFieldDef/ILPropertyDef/ILEventDef arrays and called the txIL* factory to create a brand-new wrapper object:

inp.Fields.Entries |> Array.tryPick (fun p -> if p.Name = name then Some (txILFieldDef this p) else None)

This created a new allocation on every call even when fieldDefs/propDefs/eventDefs lazy arrays were already fully computed (e.g. after a preceding GetFields(bindingFlags) call).

After: the methods call Force() on the already-computed lazy arrays and filter by name. When the caches are warm (the common case during compilation) zero new wrappers are allocated, and callers get consistent object identity across GetFields/GetField pairs.

Test Status

✅ 126/126 tests pass (dotnet test tests/FSharp.TypeProviders.SDK.Tests.fsproj).

Generated by 🌈 Repo Assist, see workflow run. Learn more.

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/repo-assist.md@e1ecf341a90b7bc2021e77c58685d7e269e20b99

…getTypeDefinition member lookups

ILMethodDefs.lmap was building per-name buckets with repeated Array.append
prepends — O(n²) total allocations for a type with N methods. Switch to a
ResizeArray accumulator that produces each bucket array in a single ToArray call.

TargetTypeDefinition.GetField / GetPropertyImpl / GetEvent previously bypassed
the per-type lazy caches (fieldDefs / propDefs / eventDefs) and called the tx*
wrapper factories on every invocation.  Using Force() on the already-computed
lazy arrays returns the same cached wrapper objects, avoids redundant allocations
when the arrays are already warm, and guarantees consistent object identity
across GetFields/GetField call pairs.

126/126 tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dsyme
dsyme marked this pull request as ready for review April 3, 2026 12:49
@dsyme
dsyme merged commit 6c8ab20 into master Apr 3, 2026
2 checks passed
github-actions Bot added a commit that referenced this pull request Apr 7, 2026
- Performance: O(1) convTypeRef assembly-name lookup (#493)
- Performance: O(N) ILMethodDefs index construction; lazy caches in TargetTypeDefinition (#497)
- Refactor: save-based caching for GetField/GetEvent/GetNestedType; Dictionary in ILNestedExportedTypesAndForwarders (#498)
- CI: NuGet and FAKE build caching (#495)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dsyme pushed a commit that referenced this pull request Apr 7, 2026
🤖 *This PR was created by [Repo
Assist](https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24058266578).*

## Summary

Bumps the version to **8.5.0** by adding an entry to `RELEASE_NOTES.md`
covering all changes merged since 8.4.0:

- **Performance**: O(1) assembly-name dictionary lookup in `convTypeRef`
(#493)
- **Performance**: Avoid O(n²) allocations in `ILMethodDefs` name-index
construction; reuse lazy caches in `TargetTypeDefinition` for
`GetField`/`GetPropertyImpl`/`GetEvent` (#497)
- **Refactor**: `save`-based caching for
`GetField`/`GetEvent`/`GetNestedType` on `ProvidedTypeDefinition`;
`Dictionary` in `ILNestedExportedTypesAndForwarders` (#498)
- **CI**: NuGet and FAKE build caching (#495)

No code changes — RELEASE_NOTES.md only.

## Test Status

✅ 126/126 tests pass (`dotnet test
tests/FSharp.TypeProviders.SDK.Tests.fsproj -c Release`).




> Generated by 🌈 Repo Assist, see [workflow
run](https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24058266578).
[Learn
more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md).
>
> To install this [agentic
workflow](https://github.com/githubnext/agentics/blob/7ee2b60744abf71b985bead4599640f165edcd93/workflows/repo-assist.md),
run
> ```
> gh aw add
githubnext/agentics@7ee2b60
> ```

<!-- gh-aw-agentic-workflow: Repo Assist, engine: copilot, model: auto,
id: 24058266578, workflow_id: repo-assist, run:
https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/24058266578
-->

<!-- gh-aw-workflow-id: repo-assist -->

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
dsyme added a commit that referenced this pull request Jul 29, 2026
…er body (issue #341) (#520)

## Problem

Issue #341 reported that generative compilation
(`AssemblyCompiler.Compile`) is still very slow for large providers
(SwaggerProvider + Stripe schema), even after the memoization work in
#443/#457/#471/#493/#497/#502/#509/#513.

Profiling the benchmark suite with dotnet-trace showed that with those
caches in place, **~80% of `Compile` time was FSharp.Core quotation
deserialization** (`PatternsModule.deserialize` → `instMeth` →
`GetGenericArguments`/`MakeGenericType`), attributed to the
`CodeGenerator` constructor.

Root cause: `CodeGenerator` defines ~63 active patterns from quotation
literals — `let (|Addition|_|) = (|SpecificCall|_|) <@ (+) @>`, `<@ max
@>`, `<@ sqrt @>`, etc. Evaluating a quotation literal deserializes it
(reflection-binding every method it mentions), and a `CodeGenerator` is
constructed **once per emitted member body**. A provider with 10,000
members therefore deserialized those quotations ~630,000 times.
`lessThan` was worse still: it deserialized `<@@ (<) @@>` on every call.

## Fix

* New `CodeGenPatterns(convTypeToTgt)` type holding all these patterns
as `member val` properties, so they are computed once at construction (a
plain property getter would re-evaluate per access).
* `AssemblyCompiler.Compile` constructs one instance and passes it to
every `CodeGenerator`; the constructor now just re-binds the local
active patterns from it, so the large `emitExpr` match code is
untouched.
* `lessThan` uses a precomputed generic method definition.

## Results

Quick benchmark (`benchmarks -- --quick`, ms/iter, before → after):

| Scenario | Before | After | Speedup |
|---|---|---|---|
| Methods 50×20 | 880 | 86 | 10× |
| Methods 200×20 | 2893 | 180 | 16× |
| Methods 1000×10 | 6892 | 535 | 13× |
| Props 500×10 | 8074 | 1259 | 6.4× |
| ComplexBody 300×10 | 2528 | 158 | 16× |
| Nested 100×4×5 | 1668 | 127 | 13× |

End-to-end with the exact repro from #341 (SwaggerProvider built from
source against this branch, Stripe `spec3.sdk.json`, `dotnet fsi`):
**62–72 s → 19–20 s** wall clock; the remainder is dominated by schema
download, OpenApi parsing and fsi startup rather than the TPSDK.

Re-profiling after the change confirms the SDK-side deserialization is
gone; the remaining `deserialize` samples come from the provider
author's own `<@@ ... @@>` literals in `invokeCode` (once per member,
inherent to user code).

## Testing

* Full test suite passes: 160/160.
* Benchmark suite builds and runs (numbers above).
* SwaggerProvider builds unmodified against the patched
`ProvidedTypes.fs` and the generated Stripe client works
(`Stripe.Client()` constructed successfully).

Fixes #341 (the remaining slowness reported there after the earlier
rounds of caching).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Don Syme <dsyme@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant