Skip to content

Commit 6c8ab20

Browse files
[Repo Assist] perf: avoid O(n²) allocations in ILMethodDefs; use lazy caches in TargetTypeDefinition member lookups (#497)
🤖 *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: ```fsharp 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: ```fsharp 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](https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/23928413736). [Learn more](https://github.com/githubnext/agentics/blob/main/docs/repo-assist.md). > > To install this [agentic workflow](https://github.com/githubnext/agentics/blob/e1ecf341a90b7bc2021e77c58685d7e269e20b99/workflows/repo-assist.md), run > ``` > gh aw add githubnext/agentics@e1ecf34 > ``` <!-- gh-aw-agentic-workflow: Repo Assist, engine: copilot, model: auto, id: 23928413736, workflow_id: repo-assist, run: https://github.com/fsprojects/FSharp.TypeProviders.SDK/actions/runs/23928413736 --> <!-- 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>
1 parent 56473b1 commit 6c8ab20

File tree

1 file changed

+11
-13
lines changed

1 file changed

+11
-13
lines changed

src/ProvidedTypes.fs

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2881,12 +2881,16 @@ module internal AssemblyReader =
28812881
type ILMethodDefs(larr: Lazy<ILMethodDef[]>) =
28822882

28832883
let lmap = lazy (
2884-
let m = Dictionary()
2884+
// Use ResizeArray during construction to avoid O(n^2) allocations when
2885+
// building per-name buckets for overloaded methods.
2886+
let tmp = Dictionary<string, ResizeArray<ILMethodDef>>()
28852887
for y in larr.Force() do
28862888
let key = y.Name
2887-
match m.TryGetValue key with
2888-
| true, lmpak -> m.[key] <- Array.append [| y |] lmpak
2889-
| false, _ -> m.[key] <- [| y |]
2889+
match tmp.TryGetValue key with
2890+
| true, ra -> ra.Add y
2891+
| false, _ -> tmp.[key] <- ResizeArray [y]
2892+
let m = Dictionary<string, ILMethodDef[]>(tmp.Count)
2893+
for kvp in tmp do m.[kvp.Key] <- kvp.Value.ToArray()
28902894
m)
28912895
let getmap() = lmap.Value
28922896

@@ -8058,19 +8062,13 @@ namespace ProviderImplementation.ProvidedTypes
80588062
md |> Option.map (txILMethodDef this) |> Option.toObj
80598063

80608064
override this.GetField(name, _bindingFlags) =
8061-
inp.Fields.Entries
8062-
|> Array.tryPick (fun p -> if p.Name = name then Some (txILFieldDef this p) else None)
8063-
|> Option.toObj
8065+
fieldDefs.Force() |> Array.tryFind (fun f -> f.Name = name) |> Option.toObj
80648066

80658067
override this.GetPropertyImpl(name, _bindingFlags, _binder, _returnType, _types, _modifiers) =
8066-
inp.Properties.Entries
8067-
|> Array.tryPick (fun p -> if p.Name = name then Some (txILPropertyDef this p) else None)
8068-
|> Option.toObj
8068+
propDefs.Force() |> Array.tryFind (fun p -> p.Name = name) |> Option.toObj
80698069

80708070
override this.GetEvent(name, _bindingFlags) =
8071-
inp.Events.Entries
8072-
|> Array.tryPick (fun ev -> if ev.Name = name then Some (txILEventDef this ev) else None)
8073-
|> Option.toObj
8071+
eventDefs.Force() |> Array.tryFind (fun ev -> ev.Name = name) |> Option.toObj
80748072

80758073
override this.GetNestedType(name, _bindingFlags) =
80768074
inp.NestedTypes.TryFindByName(UNone, name) |> Option.map (asm.TxILTypeDef (Some (this :> Type))) |> Option.toObj

0 commit comments

Comments
 (0)